Skip to content
Kotlin Exception Handling: try, catch, throw explained

24 Minutes

Kotlin Exception Handling: try, catch, throw explained

Fix Bugs Faster! Log Collection Made Easy

Get started

Introduction to exception handling in Kotlin

In Kotlin and other programming languages, exceptions are unexpected events that can stop a program if left unmanaged. They can do some serious damage for our apps, so we need to know how to flag and manage them effectively.

Kotlin offers some powerful tools for exception-handling, with concise syntax and smooth Java interoperability. It supports try-catch for handling errors, finally for cleanup, and custom exceptions for specific cases.

This makes it easier for us to keep apps stable, but we still need to understand how to utilize these tools, when to use them, and how to go beyond Kotlin’s default toolkit to create rules that are adapted to our own specific circumstances.

In this guide we’ll cover all the essentials of exception-handling, including:

  • What exceptions are, and why they matter.
  • The main exception classes, their hierarchy, and common types.
  • How to throw exceptions with the throw keyword and precondition functions (require, check, error).
  • How to handle exceptions using try, catch, and finally.
  • Advanced topics such as creating custom exceptions and using the Nothing type.
  • Best practices, common mistakes, and FAQs.

By the end, you should have a strong grasp of exceptions and this will make your Kotlin code more stable and resilient. So let’s break it down!

First: what is an exception in Kotlin?

An exception in Kotlin is an event that disrupts the normal flow of a program. Exceptions usually appear at runtime, when the code encounters something unexpected or invalid.

Instead of just failing silently, Kotlin raises an exception to signal that the program cannot continue unless the problem is handled.

Examples of situations that may throw exceptions include:

  • Dividing a number by zero.
  • Accessing an invalid index in a list.
  • Trying to read a file that doesn’t exist.
  • Calling a method on a null reference.

Without proper handling, these exceptions will stop execution and crash the application.

Why exception handling is important in Kotlin

On one level, good exception-handling avoids costly and potentially destructive crashes, which is obviously vital. But more generally, it keeps applications reliable and easier to maintain, delivering the following specific benefits:

  • Program stability: By preventing crashes, we enhance the user experience and give our users greater confidence in our app.
  • Debugging: Clear error messages and logs make issues faster to diagnose, saving time for us and our audience.
  • Resource management: The smooth functioning of our app means that its nuts and bolts – the files, memory, or network connections – will be released in a proper, timely manner.
  • Security: Unhandled errors can compromise our data and cause data leaks.

In the case of coroutines (regular functions that execute in a particular context), Kotlin also provides tools for handling exceptions in asynchronous code, making robust error management an essential part of modern Kotlin development.

Exception hierarchy in Kotlin

Kotlin inherits the Java exception hierarchy, starting from the Throwable class.

From there, the hierarchy splits into two main categories:

  • Error → This represents serious issues that the program cannot handle, such as OutOfMemoryError or StackOverflowError. These are rarely managed in application code.
  • Exception → This represents problems that can be handled and managed in code, such as invalid input or failed I/O operations. We typically catch exceptions with try-catch blocks.

Now we’ve outlined this core distinction, let’s drill down to the next level: checked vs unchecked exceptions and the most common exception classes in Kotlin.

Are there checked exceptions in Kotlin?

In Java, Kotlin’s predecessor, checked exceptions had to be explicitly declared and caught, whereas unchecked exceptions did not. Kotlin inherits these categories, but in Kotlin, all exceptions are effectively unchecked: the compiler never forces us to declare or catch them. This simplifies exception handling, making Kotlin code less verbose and easier to maintain.

  • In the case of Checked exceptions like IOException and SQLException, Kotlin will not enforce the process of catching and declaration.
  • In the case of Unchecked exceptions like NullPointerException or IllegalArgumentException, these are not checked at compile time.

How to handle exceptions in Kotlin with try-catch

Kotlin try catch syntax explained

try-catch is Kotlin’s dedicated block for handling exceptions, so it’s obviously fundamental to our exception-handling regime.

  • The try block contains code that might fail.
  • The catch block defines how to handle the exception, with e: ExceptionType representing the caught exception object and its type.
try {
    // Code that may throw an exception
} catch (e: ExceptionType) {
    // Code to handle the exception
}

In this example, dividing by zero would normally stop execution. Instead, the catch block intercepts the ArithmeticException and safely returns null.

fun divide(a: Int, b: Int): Int? {
    return try {
        a / b
    } catch (e: ArithmeticException) {
        println("Cannot divide by zero")
        null  // return null instead of crashing
    }
}

fun main() {
    println(divide(10, 2))  // Output: 5
    println(divide(10, 0))  // Output: Cannot divide by zero
                            //         null
}

Kotlin catch multiple exceptions

This is one of the really cool bits of Kotlin.

Sometimes the same piece of code can fail in more than one way. However, in Kotlin, we can stack multiple catch blocks to react differently, depending on what went wrong. This keeps our error messages clear and our program easier to debug.

For example, a calculation might fail because the divisor is zero, or because the input cannot be converted into a number. We can add a specific catch for each case, plus a generic Exception block as a fallback for anything unexpected.

fun divide(input: String, b: Int): Int? {
    return try {
        val a = input.toInt()       // may throw NumberFormatException
        a / b                       // may throw ArithmeticException
    } catch (e: NumberFormatException) {
        println("Invalid input: not a number")
        null
    } catch (e: ArithmeticException) {
        println("Cannot divide by zero")
        null
    } catch (e: Exception) {
        println("Something went wrong: ${e.message}")
        null
    }
}

fun main() {
    println(divide("10", 2))   // Output: 5
    println(divide("10", 0))   // Output: Cannot divide by zero
                               //         null
    println(divide("oops", 2)) // Output: Invalid input: not a number
                               //         null
}

How to throw exceptions in Kotlin

Ok, we’ve covered the core exception-handling tools. Now let’s go a stage further, and look at how to throw an exception in Kotlin (in other words, deliberately signal that an error has occurred).

When code reaches a situation it cannot handle, we use the throw keyword to stop normal execution and notify the caller about the problem, which ensures that errors are not ignored.

Throwing exceptions is really useful when:

  • A function receives invalid input.
  • An object is in an invalid state.
  • A resource like a file, memory, or network is unavailable.

By throwing exceptions, the calling code gets a clear signal that it must handle the failure. This keeps programs predictable and simplifies the maintenance process, while allowing developers to define custom exceptions for more meaningful error messages.

Common exception classes

Kotlin provides a wide range of built-in exceptions that developers encounter frequently. These exceptions typically fall into two categories:

  • Automatic exceptions: These are thrown by the Kotlin runtime when an invalid operation occurs (like the cases we mentioned earlier – dividing by zero, accessing a null value, etc).
  • Manual exceptions: These are explicitly thrown by developers using the throw keyword or helper functions like require() and check() to enforce rules in the code.
Exception ClassWhen It Occurs
NullPointerExceptionAccessing a property or method on a null object
IndexOutOfBoundsExceptionAccessing an invalid index in a list or array
IllegalArgumentExceptionPassing an argument that a function cannot accept
IllegalStateExceptionPerforming an action when an object is not in a valid state
ArithmeticExceptionInvalid arithmetic operations, e.g. division by zero
IOExceptionInput/output problems, like trying to read a missing file
SocketExceptionNetwork-related errors when a socket fails
SQLExceptionDatabase access errors or invalid queries

Kotlin exception examples

// NullPointerException: trying to access a property on null
val text: String? = null
println(text!!.length)

// IndexOutOfBoundsException: accessing an index that doesn’t exist
val list = listOf(1, 2)
println(list[5])

// ArithmeticException: dividing a number by zero
val result = 10 / 0

// IOException: trying to read a file that does not exist
java.io.File("missing.txt").readLines()

Using the throw keyword in Kotlin

We manually throw an exception using the throw keyword, followed by an exception object:

throw Exception("Something went wrong")

Simple, right? Here’s a practical example:

fun validateAge(age: Int) {
if (age < 18) {
throw IllegalArgumentException("Age must be at least 18")
}
println("Valid age: $age")
}
fun main() {
validateAge(20)   // ✅ prints "Valid age: 20"
validateAge(15)   // ❌ throws IllegalArgumentException
}

How precondition functions throw exceptions in Kotlin

Precondition functions in Kotlin are a utility class that allow us to throw a certain string of code once a certain precondition has occurred. This makes it easier to enforce rules without writing verbose checks.

Normally, we might write:

if (age < 0) {
    throw IllegalArgumentException("Age must be non-negative")
}

With precondition functions like require, check, and error, the same logic becomes cleaner and more expressive. They automatically throw the right exception when a condition fails, making our code shorter, easier to read, and less error-prone.

require(): validate input

The require() function is used in Kotlin to validate arguments passed into a function. If the condition is false, it automatically throws an IllegalArgumentException.

Using require() ensures invalid inputs are rejected early, and allows us to check inputs before the rest of our code runs.

fun setAge(age: Int) {
    require(age >= 0) { "Age must be non-negative" }
    println("Age set to $age")
}

fun main() {
    // Valid input
    setAge(25)

    // Invalid input → require() fails
    try {
        setAge(-5)
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}

requireNotNull(): validate non-null values

requireNotNull() is used when a value must not be null for our code to work. Instead of risking a crash later, it checks right away and throws an IllegalArgumentException if the value is null. This makes our intent clear: the function cannot continue without a valid value.

We use requireNotNull() when we expect a value to always exist, and want to fail fast if it doesn’t.

fun printName(name: String?) {
    val validName = requireNotNull(name) { "Name cannot be null" }
    println("Hello, $validName")
}

fun main() {
    // Valid input
    printName("Alice")

    // Invalid input → requireNotNull() fails
    try {
        printName(null)
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}

check(): validate state

The check() function ensures a particular part of our program is in the right condition before continuing: for example, that a bank account has enough balance, or that a game character is still alive. If the condition is false, it throws an IllegalStateException.

This is different from require(), which is used to validate function inputs.

  • require() → check arguments passed into a function
  • check() → check the current state of an object or variable
class BankAccount(private var balance: Int) {
    fun withdraw(amount: Int) {
        check(balance >= amount) { "Insufficient funds" }
        balance -= amount
        println("Withdrew $amount, remaining balance: $balance")
    }
}

fun main() {
    val account = BankAccount(100)

    // First withdrawal works
    account.withdraw(60)

    // Second withdrawal fails because state is invalid
    try {
        account.withdraw(50)
    } catch (e: IllegalStateException) {
        println("Error: ${e.message}")
    }
}

Use check() to stop execution when the current state makes an operation invalid, even if the input looks fine.

checkNotNull(): validate state

The checkNotNull() function is used when a value that belongs to an object’s state must not be null at a certain point in execution. This is vital in all kinds of scenarios, such as when we inflate the layout, when we call displayProfile and when we call performClick().

If the value is null, it throws an IllegalStateException.

This differs from requireNotNull(), which is meant for function arguments.

  • requireNotNull() → ensure an argument passed into a function is not null
  • checkNotNull() → ensure an object’s state is not null when used
class Config(var apiKey: String? = null) {
    fun connect() {
        val key = checkNotNull(apiKey) { "API key has not been set" }
        println("Connecting with key: $key")
    }
}

fun main() {
    val config = Config()

    // Invalid state → apiKey not set
    try {
        config.connect()
    } catch (e: IllegalStateException) {
        println("Error: ${e.message}")
    }

    // Fix state, then works
    config.apiKey = "SECRET123"
    config.connect()
}

We should use checkNotNull() when the object has already been initialized and execution cannot continue safely without a non-null value.

error(): signal illegal conditions

The error() function in Kotlin is a quick way to stop execution when the program reaches a condition that should never happen. Unlike require() and check(), which validate inputs or states, error() is often used for impossible or unsupported cases. It always throws an IllegalStateException with the message you provide.

A common example is validating months: since a year only has 12 months, anything above that number must be invalid!

fun getMonthName(month: Int): String {
    return when (month) {
        1 -> "January"
        2 -> "February"
        3 -> "March"
        4 -> "April"
        5 -> "May"
        6 -> "June"
        7 -> "July"
        8 -> "August"
        9 -> "September"
        10 -> "October"
        11 -> "November"
        12 -> "December"
        else -> error("Invalid month: $month")
    }
}

fun main() {
    // Valid input
    println(getMonthName(7))

    // Invalid input → error() fails
    try {
        println(getMonthName(15))
    } catch (e: IllegalStateException) {
        println("Error: ${e.message}")
    }
}

Use error() when a branch of code should never be valid, making mistakes obvious during testing.

Kotlin precondition functions comparison: A quick table

Function + ExceptionWhen to use + Example
require()IllegalArgumentExceptionValidate function arguments (e.g. age must not be negative).
check()IllegalStateExceptionValidate object state (e.g., account must have enough balance to withdraw).
requireNotNull()IllegalArgumentExceptionEnsure an argument is not null (e.g., a name must be provided by the caller).
checkNotNull()IllegalStateExceptionEnsure an object’s state is not null (e.g., configuration must be initialized).
error()IllegalStateExceptionMark impossible or unsupported cases (e.g., month outside 1–12).

The Nothing type in Kotlin

In Kotlin, even code that never returns has a type. The special type Nothing represents execution paths that cannot produce a value. We don’t have to write Nothing directly, it simply shows up automatically when we use functions that always throw an exception.

  • error() uses Nothing to mark impossible conditions.
  • TODO() uses Nothing to mark unfinished code.

This helps the compiler know: “Execution stops here, don’t expect a result.”

fun calculateTax(income: Double): Double {
    TODO("Tax calculation not implemented yet")
    // Always throws NotImplementedError → return type is Nothing
}

fun main() {
    calculateTax(50000.0)
    // Program crashes with: NotImplementedError
}

How to read stack trace lines and what to do

A stack trace is a report generated by the runtime whenever an error or exception occurs. It shows the sequence of function calls that led to the problem, helping us locate the exact spot where our code failed.

For example, running this code:

fun main() {
    throw ArithmeticException("This is an arithmetic exception!")
}

produces the following stack trace:

Exception in thread "main" java.lang.ArithmeticException: This is an arithmetic exception!
    at MainKt.main(Main.kt:2)
    at MainKt.main(Main.kt)
  • First line → exception type (ArithmeticException), thread (main), and message (This is an arithmetic exception!).
  • Lines starting with at → stack frames. Each frame shows the function, file, and line number.
  • Task → we need to focus on the first frames pointing to the code, since they reveal where the issue originated.

Reading stack traces this way lets us quickly trace bugs and fix errors with confidence.

How to create custom exceptions in Kotlin

As we’ve seen, Kotlin provides a number of built-in exceptions out of the box. But sometimes these built-in exceptions, like IllegalArgumentException, are too vague and don’t give us the information we require.

In these cases, when we need clearer error reporting in our application, we can define our own custom exception by creating a class that extends Exception. This gives us more meaningful messages and helps organize error-handling.

1. Extend from a base exception class

In Kotlin, all exceptions come from the root class Throwable.

From there, you have two main branches: Exception and RuntimeException.

The difference is mostly historical from Java:

Exception was meant for “checked” exceptions you had to handle, while RuntimeException was for unchecked ones. Since Kotlin treats all exceptions as unchecked, the choice is mostly about convention.

  • If the error is part of normal application logic (like invalid input or a failed payment), extend Exception.
  • If it signals a programming error or something that usually shouldn’t be caught (like null pointer–style issues), extend RuntimeException.

Once you pick the right base, you simply create your own sub-class with a clear name and optional constructors, making it easy to throw and catch domain-specific errors.

// Application-level error
class NegativeBalanceException(message: String) : Exception(message)

// Programming-level error
class BankConfigException(message: String) : RuntimeException(message)

2. Add constructors

A constructor is a special function that runs when we create an object. In custom exceptions, constructors decide what information the exception carries. There are usually two useful kinds:

  1. Message constructor – lets us pass in a string describing the problem.
  2. Message + cause constructor – lets us pass both a description and another exception that triggered it, so we don’t lose the original error.

Inside the constructor, super(...) means we’re calling the constructor of the parent class (Exception) and passing the information up, so it’s stored and later shown in the error message or stack trace. The cause is just another exception object that explains why this one happened, giving more context when debugging.

Here’s how we can add both constructors to our NegativeBalanceException:

class NegativeBalanceException : Exception {
    // Pass only a custom error message
    constructor(message: String) : super(message)

    // Pass a custom message AND another exception as the cause
    constructor(message: String, cause: Throwable) : super(message, cause)
}

3. Throw and catch your exception

Once your custom exception is defined, you use it just like any other: throw it when something goes wrong, and catch it if you want to handle the error gracefully. The key is to give your exception a meaningful name (like InvalidUserInputException or NegativeBalanceException) so the problem is obvious when it shows up in logs or stack traces.

// Define a custom exception
class NegativeBalanceException(message: String) : Exception(message)

class BankAccount(private var balance: Int) {
    fun deposit(amount: Int) {
        if (amount < 0) {
            throw NegativeBalanceException("Cannot deposit a negative amount: $amount")
        }
        balance += amount
        println("Deposited $amount, balance: $balance")
    }
}

fun main() {
    val account = BankAccount(100)
    account.deposit(50)     // Output: Deposited 50, balance: 150
    account.deposit(-20)    // Throws NegativeBalanceException
}

When should we create a custom exception?

Most of the time, Kotlin’s built-in exceptions are good enough. But a custom exception makes sense when:

  • Domain-specific errors need to be clearer (e.g., PaymentDeclinedException).
  • Generic exceptions are too vague and don’t explain the real issue.
  • We need extra context (like an error code, user ID, or operation name).
  • We want to separate temporary vs permanent errors for better handling.
  • We’re building a public API or library where meaningful exceptions improve usability.

Best practices for handling exceptions in Kotlin

Ok, so we’re nearly through the article now.

We’ve already talked about the nuts and bolts, so you should have a good grasp of the basics. But effective exception-handling isn’t just about setting the right triggers—it’s about following a clear process that makes our code more robust and easier to maintain.

Here are some of the key principles to keep in mind at all times:

  • Catch specific exceptions instead of using a generic Exception.
  • Avoid silent failures where errors are ignored.
  • Use finally for cleanup to release resources safely.
  • Throw meaningful exceptions with clear messages.
  • Log exceptions to help with debugging and monitoring.
  • Use crash reporting tools to catch what slips through.

Now let’s look at each of these principles in detail, with examples.

Catch specific exceptions

Each different exceptions should be handled in a bespoke manner. That way, catching them and dealing with their specific case is more manageable, since the error message has also specified what went wrong.

By implementing catching-specific exception practice, we can write a special response for every error condition.

fun readMyFile(fileName: String) {
    try {
        val fileObj = File(fileName)
        val reader = BufferedReader(FileReader(fileObj))
        println(reader.readLine())
        reader.close()
    } catch (e: FileNotFoundException) {
        println("File not found: ${e.message}")
    } catch (e: IOException) {
        println("An I/O error occurred: ${e.message}")
    }
}

Avoid silent failures

A silent failure is when an exception is caught and not handled or logged properly, so you may never learn that something has gone wrong. Exceptions should not be caught only to throw the same exception (or re-wrap an existing one) without any other action or logging in place, as it makes troubleshooting harder.

The wrong way:

try {
    // Some code that may throw an exception
} catch (e: Exception) {
    // Do nothing
}

The right way:

try {
    // Some code that may throw an exception
} catch (e: Exception) {
    println("An error occurred: ${e.message}")
}

Use Finally for resource cleanup

The finally block is a code that will always be executed whether an exception has been thrown or not. This is helpful with clean-up resources, such as closing files and network connection.

fun readMyFile(fileName: String) {
    var reader: BufferedReader? = null
    try {
        val fileObj = File(fileName)
        reader = BufferedReader(FileReader(fileObj))
        println(reader.readLine())
    } catch (e: IOException) {
        println("An error occurred: ${e.message}")
    } finally {
        reader?.close()
        println("Reader closed")
    }
}

Throw meaningful exceptions

When throwing exceptions, exception messages should describe the issue. It will help others to gain a clear idea of what happened and how to resolve it.

fun setAge(age: Int) {
    if (age < 0) {
        throw IllegalArgumentException("Age cannot be negative")
    }
    println("Age is set to $age")
}

fun main() {
    try {
        setAge(-5)
    } catch (e: IllegalArgumentException) {
        println(e.message) // Output: Age cannot be negative
    }
}

Logging exceptions

We should always log/record the exceptions so that when something goes wrong, we can better understand what actually happened.

Log exception details, including stack trace, help us see and track the error. We can use any logging framework for this.

import java.util.logging.Logger

val logger = Logger.getLogger("Logger")

fun readFile(fileName: String) {
    try {
        val file = File(fileName)
        val reader = BufferedReader(FileReader(file))
        println(reader.readLine())
        reader.close()
    } catch (e: IOException) {
        logger.severe("An I/O error has occurred: ${e.message}")
    }
}

fun main() {
    readFile("exampleFile.txt")
    // Check the log for error information
}

Using a crash reporting tool like Bugfender

Even with solid exception handling, apps can still crash in ways we didn’t anticipate. That’s why using a crash reporting tool is essential: it reveals uncaught exceptions and overlooked scenarios we’d miss in testing.

Bugfender makes this easy. Unlike traditional logging, which stays on the device, Bugfender automatically captures logs and crashes and sends them to a real-time dashboard. You can see the error, the stack trace, and even the user’s interactions leading up to it.

The setup only takes a few minutes: install the SDK, and Bugfender starts recording runtime exceptions right away. The result is faster debugging, better stability, and a smoother user experience.

Try Bugfender free at dashboard.bugfender.com/signup.

To sum up

Exception handling in Kotlin is about more than avoiding crashes, it’s about writing code that is predictable, maintainable, and user-friendly.

We explored how to throw and catch exceptions, the role of require, check, and their NotNull variants, and how to read stack traces effectively. We also covered best practices such as catching specific exceptions, avoiding silent failures, and using finally for safe cleanup.

By applying these techniques, we not only prevent runtime errors but also improve debugging, resource management, and application stability.

For deeper visibility into errors in real-world apps, tools like Bugfender capture crashes and logs remotely, helping developers troubleshoot faster and deliver more reliable experiences.

That’s it for the Kotlin exception-handling. Happy Coding!

Kotlin exception handling FAQs

What is the alternative to try-catch in Kotlin?

Besides try-catch, Kotlin offers precondition functions like require(), check(), and error() to fail fast. It also provides runCatching {} which wraps results in a Result type instead of throwing exceptions.

What is runCatching in Kotlin?

runCatching {} executes a block of code and returns a Result. On success, it holds the value; on failure, it holds the exception. This enables functional-style error handling with methods like onSuccess and onFailure.

Can finally be used without catch?

Yes. You can use try-finally without catch. The exception will still propagate, but the finally block ensures cleanup code always runs.

Should we catch Throwable in Kotlin?

No. Throwable includes both Exception and Error. Errors like OutOfMemoryError usually can’t be recovered from and should not be caught in normal code. Catch only the exceptions you can meaningfully handle.

What is the difference between require() and check() in Kotlin?

Use require() to validate function inputs (throws IllegalArgumentException), and check() to validate object state (throws IllegalStateException). This makes our intent clearer and error messages more meaningful.

What is the difference between requireNotNull() and checkNotNull()?

Both ensure a value is not null and return it. The difference is the exception type: requireNotNull() throws IllegalArgumentException, while checkNotNull() throws IllegalStateException. Use the one that best fits your context (input vs. state).

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Sachin Siwal

Sachin is an accomplished iOS developer and seasoned technical manager, combining 12 years of mobile app expertise with a knack for leading and delivering innovative solutions. You can contact him on Linkedin

Join thousands of developers
and start fixing bugs faster than ever.