ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Kotlin Tips and Tricks You May Not Know: #7 — Goodbye try-catch, Hello runCatching!

Elena van Engelen - Maslova
ProAndroidDev
Published in
9 min readFeb 27, 2025

--

Introduction

Returning a Result for Safer Code

fun parseNumber(input: String): Result<Int> {
return runCatching { input.toInt() }
}
val result = parseNumber("123")
result.onSuccess { println("Parsed number: $it") }
.onFailure { println("Failed to parse number: ${it.message}") }

runCatching as a Scope Function

data class Order(val id: String, val quantity: Int)
val order = Order(id = "123", quantity = 0)

val result = order.runCatching {
100 / quantity // This will cause a division by zero
}.onFailure { e ->
println("An error occurred: ${e.message}")
}.onSuccess { value ->
println("Computation successful: $value")
}
println("Result object: $result")

Replacing Try-Catch with an Empty Catch Block

fun fireAndForget() {
try {
riskyFunction()
} catch (t: Throwable) {
// Ignore
}
}
fun fireAndForget() {
runCatching { riskyFunction() }
}

Falling Back to a Default Value

fun parseNumberWithDefault(input: String): Int {
return try {
input.toInt()
} catch (t: Throwable) {
0 // Default value for invalid numbers
}
}
fun parseNumberWithDefault(input: String): Int {
return runCatching { input.toInt() }.getOrElse { 0 }
}

Rethrow Original Exceptions with a getOrThrow

fun parseNumberWithExceptions(input: String): Int {
return try {
input.toInt()
} catch (e: NumberFormatException) {
logger.error(e){"Failed parsing integer"} // Log error
throw e // Unexpected exceptions are rethrown
}
}
fun parseNumberWithExceptions(input: String): Int {
return runCatching { input.toInt() }
.onFailure { e -> logger.error(e){"Failed parsing integer"}} // Log error
.getOrThrow()
}

Handling Nested Exceptions with runCatching

fun processFile(path: String): ProcessedData {
return try {
val content = File(path).readText()
try {
val json = parseJson(content)
try {
processData(json)
} catch (e: Exception) {
logger.error(e) { "Failed to process data" }
throw e
}
} catch (e: Exception) {
logger.error(e) { "Failed to parse JSON" }
throw e
}
} catch (e: Exception) {
logger.error(e) { "Failed to read file: $path" }
throw e
}
}
fun processFile(path: String): ProcessedData {
val content = try {
File(path).readText()
} catch (e: Exception) {
logger.error(e) { "Failed to read file: $path" }
throw e
}

val json = try {
parseJson(content)
} catch (e: Exception) {
logger.error(e) { "Failed to parse JSON" }
throw e
}

return try {
processData(json)
} catch (e: Exception) {
logger.error(e) { "Failed to process data" }
throw e
}
}
fun processFile(path: String): ProcessedData {
return runCatching { File(path).readText() }
.onFailure { e -> logger.error(e) { "Failed to read file: $path" } }
.mapCatching { content -> parseJson(content) }
.onFailure { e -> logger.error(e) { "Failed to parse JSON" } }
.mapCatching { json -> processData(json) }
.onFailure { e -> logger.error(e) { "Failed to process data" } }
.getOrThrow()
}

Handling Multiple Exception Types

fun readFile(path: String): String {
return try {
File(path).readText()
} catch (e: FileNotFoundException) {
logger.error(e) { "File not found: $path" }
throw e
} catch (e: IOException) {
logger.error(e) { "Failed to read file: $path" }
throw e
}
}

fun readFile(path: String): String {
return runCatching { File(path).readText() }
.onFailure { e ->
when (e) {
is FileNotFoundException -> logger.error(e) { "File not found: $path" }
is IOException -> logger.error(e) { "Failed to read file: $path" }
}
}.getOrThrow()
}

Replacing Try-Catch-Finally Blocks

fun readFileWithTryCatch(path: String): String {
val reader = File(path).bufferedReader()
return try {
reader.readText()
} catch (e: Exception) {
logger.error(e){"Failed to read file: ${e.message}"}
throw e
} finally {
reader.close() // Must be manually closed
}
}
fun readFileWithUse(path: String): String {
return runCatching {
File(path).bufferedReader().use { it.readText() } // Auto-closes reader
}.onFailure { logger.error(it){"Failed to read file: ${it.message}" }}
.getOrThrow()
}

Handling Coroutines and Errors with runCatching

Avoiding Cancellation Issues in Coroutines

inline fun <reified E : Throwable, T> Result<T>.onFailureOrRethrow(action: (Throwable) -> Unit): Result<T> {
return onFailure { if (it is E) throw it else action(it) }
}

inline fun <T> Result<T>.onFailureIgnoreCancellation(action: (Throwable) -> Unit): Result<T> {
return onFailureOrRethrow<CancellationException, T>(action)
}
val result = runCatching {
apiService.fetchDataFromServer()
}.onFailureIgnoreCancellation {
println("Handled non-cancellation error: ${it.message}")
}

Avoiding System-Level Errors

inline fun <T> Result<T>.onFailureIgnoreErrors(action: (Throwable) -> Unit): Result<T> {
return onFailureOrRethrow<Error, T>(action)
}
val result = runCatching {
riskyOperation()
}.onFailureIgnoreErrors {
println("Handled exception: ${it.message}")
}

Static Analysis for Better Exception Handling

Conclusion

Tip Recap:

References

--

--

Responses (4)