Error handling is a fundamental aspect of any robust application. Kotlin, being a modern programming language, offers several mechanisms to handle errors effectively and gracefully.
1. Exception Handling: Try-Catch Blocks
Kotlin uses try-catch
blocks for handling exceptions, similar to Java. This mechanism is straightforward and familiar to developers coming from Java or similar languages.
fun divide(a: Int, b: Int): Int { return try { a / b } catch (e: ArithmeticException) { println("Error: ${e.message}") 0 // Return a default value } } fun main() { val result = divide(10, 0) println("Result: $result") }
In this example, if a division by zero occurs, the catch
block handles the ArithmeticException
and provides a default value.
Explanation:
- The
try
block contains the code that might throw an exception. - The
catch
block handles specific exceptions, likeArithmeticException
in this case. - This approach ensures that the program doesn’t crash and instead recovers gracefully.
“Why did the developer bring a ladder to the code review? To handle high-level exceptions!”
2. Checked vs. Unchecked Exceptions
Unlike Java, Kotlin doesn’t differentiate between checked and unchecked exceptions. All exceptions are unchecked, meaning that the compiler doesn’t force you to handle them. This design simplifies code and avoids boilerplate.

Explanation:
- Checked exceptions require explicit handling, often leading to verbose code.
- Kotlin’s approach aligns with modern development practices, emphasizing developer responsibility over compiler enforcement.
3. Custom Exceptions
Kotlin allows developers to define custom exceptions for more granular error handling.
class InvalidInputException(message: String) : Exception(message) fun validateInput(input: Int) { if (input < 0) { throw InvalidInputException("Input must be non-negative") } } fun main() { try { validateInput(-5) } catch (e: InvalidInputException) { println("Caught Exception: ${e.message}") } }
Explanation:
- Custom exceptions provide clarity by allowing you to communicate domain-specific issues.
- This makes debugging easier as the exception conveys exactly what went wrong.
Throwing custom exceptions is like saying, “This is my circus, and these are my monkeys.”
Custom exceptions help in creating domain-specific error-handling mechanisms.
4. Null Safety and the Elvis Operator
Kotlin’s null safety features significantly reduce the chances of NullPointerException
. The Elvis operator (?:
) provides a concise way to handle nullable types.
fun getLength(str: String?): Int { return str?.length ?: 0 } fun main() { println(getLength(null)) // Outputs: 0 }
Explanation:
- The
?.
operator safely accesses properties of nullable types. - The Elvis operator (
?:
) provides a default value if the expression to its left evaluates tonull
.
Null safety: Because Kotlin knows that “To null is human, but to not null is divine.”
Here, the Elvis operator ensures that a default value is returned if str
is null.
5. Double Bang Operator
The double bang operator (!!) is used to explicitly assert that an expression is non-null. When you use
!!
, you tell the compiler: “I know this value is not null, so proceed as if it’s non-null.”
If the value is null, it throws a NullPointerException
(NPE) at runtime. This operator is risky and should be used sparingly because it bypasses Kotlin’s built-in null safety.
Syntax of the Double Bang Operator
val nonNullableValue = nullableValue!!
- If
nullableValue
is non-null, the program continues normally. - If
nullableValue
is null, the program crashes with aNullPointerException
.
Example 1 : Basic Exception Handling with !!
To handle exceptions caused by the double bang operator, you can wrap it in a try-catch
block.
fun main() { val name: String? = null try { val length = name!!.length // Forces an exception if `name` is null println("Length of the name: $length") } catch (e: NullPointerException) { println("Caught a NullPointerException: ${e.message}") } }
Output:
Caught a NullPointerException: null
Here, the NullPointerException
is caught, and the program doesn’t crash.
When to Use !!
You should avoid !!
whenever possible. Use it only when:
- You are 100% certain that a value is non-null at runtime.
- You want the program to explicitly throw an exception when a null value is unexpected.
Example 2: Legitimate Use Case for !!
fun getNameLength(name: String?): Int { return name!!.length // You are certain `name` is not null here } fun main() { val name: String? = "Kotlin" println("Length of the name: ${getNameLength(name)}") // Works as expected }
Here, !!
is used when you are certain that name
will not be null.
The double bang operator is a powerful but dangerous tool. It’s better to rely on Kotlin’s null safety features (?.,
?:,
let,
if, etc.) to handle nulls gracefully. Reserve
!!
for situations where you need strict null-safety enforcement and are willing to risk throwing an exception.
6. Result Type
Kotlin’s Result
type is a functional approach to handle success and failure cases without exceptions.
fun safeDivide(a: Int, b: Int): Result<Int> { return if (b != 0) { Result.success(a / b) } else { Result.failure(ArithmeticException("Division by zero")) } }
fun main() { val result = safeDivide(10, 0) result.onSuccess { println("Result: $it") }.onFailure { println("Error: ${it.message}") } }
Explanation:
Result
encapsulates success or failure, avoiding the need for explicit exception handling.- The
onSuccess
andonFailure
methods allow you to handle both outcomes succinctly.
It’s like playing a game of chance: “Heads, you succeed. Tails, you divide by zero.”
7. "runCatching"
for Exception Handling
Using Result
provides a clear separation of successful and error outcomes, promoting functional programming practices.
In Kotlin, the runCatching
function is a handy utility for encapsulating the result of a block of code execution. It simplifies exception handling by wrapping the result of a code block in a Result
object. If the code executes successfully, it encapsulates the result as a Success
. If an exception occurs, it catches it and encapsulates it as a Failure
.
Here’s a breakdown of how runCatching
works:
- Input: A lambda block of code to execute.
- Output: A
Result
object that can represent either:
Success
if the block executes without exceptions.
Failure
if any exception (of typeThrowable
) occurs during execution.
This allows you to handle errors and process results more functionally.
fun main() { val result = runCatching { // Block of code that might throw an exception val number = "123".toInt() // This will succeed number / 0 // This will throw an ArithmeticException } // Handling the result result.onSuccess { value -> println("Success! The result is $value") }.onFailure { exception -> println("Failure! Caught exception: ${exception.message}") } }
Explanation:
- The block in
runCatching
contains code that attempts to:
- Parse a string to an integer (
"123".toInt()
) – successful. - Divide the number by zero — throws an
ArithmeticException
.
2. Since the second operation fails, the exception is caught, and the result is encapsulated as Failure
.
3. The result is then handled:
onSuccess block: Will execute if the operation succeeds.
onFailure block: Will execute if an exception is caught, and it provides the exception details.
Job Offers
Practical Use Case: Reading a File
fun readFileContent(fileName: String): String { return runCatching { // Simulating reading a file if (fileName.isBlank()) throw IllegalArgumentException("File name cannot be blank") "File content of $fileName" }.getOrElse { exception -> "Error occurred: ${exception.message}" } } fun main() { println(readFileContent("document.txt")) // Successful case println(readFileContent("")) // Failure case }
Output:
File content of document.txt Error occurred: File name cannot be blank
Key Benefits of runCatching:
- Concise exception handling.
- Avoids boilerplate
try-catch
blocks. - Functional programming style: chaining methods like
onSuccess
,onFailure
,map
, andgetOrElse
for clean handling of both success and failure scenarios.
Error Handling in Coroutines in Kotlin: A Deep Dive
Kotlin coroutines provide a powerful way to write asynchronous and non-blocking code. However, handling errors effectively in coroutines requires a good understanding of coroutine-specific mechanisms and best practices. Below are key strategies for error handling in coroutines.
8. Exception Handling in Coroutines
In Kotlin, exceptions in coroutines are propagated differently based on the coroutine builder used. The most commonly used builders are launch
and async
.
- launch: Exceptions are propagated to the
CoroutineExceptionHandler
. - async: Exceptions are deferred and must be caught when accessing the result.
import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { try { println("Throwing exception in launch") throw IllegalArgumentException("Exception in launch") } catch (e: Exception) { println("Caught ${e.message}") } } job.join() val deferred = async { println("Throwing exception in async") throw IllegalArgumentException("Exception in async") } try { deferred.await() } catch (e: Exception) { println("Caught ${e.message}") } }
Explanation:
launch
: Automatically propagates uncaught exceptions to its parent or aCoroutineExceptionHandler
.async
: Requires explicit handling when the result is awaited.
“Async errors: Because sometimes you just need a little delay to let the chaos unfold.”
9. CoroutineExceptionHandler
The CoroutineExceptionHandler
is a dedicated mechanism for handling uncaught exceptions in coroutines launched via launch
.
import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught ${exception.message} in CoroutineExceptionHandler") } val job = launch(handler) { throw IllegalStateException("Unhandled exception") } job.join() }
Explanation:
- Acts as a global handler for uncaught exceptions in coroutines.
- Can be attached to a
CoroutineScope
or individual coroutines.
“CoroutineExceptionHandler: The guardian angel your coroutines never knew they needed.”
10. SupervisionJob and SupervisorScope
When working with structured concurrency, SupervisorJob
and supervisorScope
allow child coroutines to fail independently without affecting others.
import kotlinx.coroutines.* fun main() = runBlocking { supervisorScope { val child1 = launch { println("Child 1 is running") throw RuntimeException("Child 1 failed") } val child2 = launch { println("Child 2 is running") delay(1000) println("Child 2 completed") } try { child1.join() } catch (e: Exception) { println("Caught exception from child 1: ${e.message}") } child2.join() } }
Explanation:
SupervisorJob
ensures that failure in one child coroutine doesn’t cancel others.supervisorScope
works similarly, providing isolation for child coroutine errors.
“Supervision: Because even coroutines need a responsible adult in the room.”
Why Exception Handling is Important in Coroutines
Exception handling in coroutines is critical due to their asynchronous and concurrent nature. Here’s why:
- Uncaught Exceptions Can Crash Your Application: Unhandled exceptions in coroutines launched in the
GlobalScope
or at the top level may lead to application crashes. This is especially risky in production environments. - Complex Error Propagation: Coroutines are designed to handle concurrency. Without proper error handling, debugging and tracing the source of exceptions can become a nightmare.
- Structured Concurrency Needs Predictability: In structured concurrency, the parent coroutine supervises its children. Unhandled exceptions in child coroutines can affect parent coroutines, causing unintended cancellations.
- Graceful Recovery: Applications often need to continue functioning even after encountering an error. Handling exceptions enables recovery mechanisms, such as retries or fallback logic.
Conclusion
Kotlin provides powerful error-handling tools, from traditional try-catch
blocks to coroutine-specific mechanisms like CoroutineExceptionHandler
and supervisorScope
. Mastering these techniques ensures your applications are robust, maintainable, and resilient to errors.
Remember, good error handling isn’t just about preventing crashes — it’s about ensuring that your application can recover gracefully and continue to provide a great user experience. And if all else fails, at least you’ll have some funny error messages to entertain your users!
This article is previously published on proandroiddev.com.