[Server-side Kotlin] Using Kotlin’s Built-in Result Type for Error Management
![Cover Image for [Server-side Kotlin] Using Kotlin’s Built-in Result Type for Error Management](/assets/common/thumbnail_default_×2.png)
Introduction
Hello. My name is Shiode, and I do payment-related backend development in the Toyota Woven City Payment Solution Development Group. As mentioned in my previous article, our group uses Kotlin for development, with Ktor as our web framework and Exposed as the ORM. We also adopt Clean Architecture in our code architecture.
Initially, we used Kotlin's Result type for error handling, but as the number of developers increased, we started seeing a mix of Result and throw used in code. Mixing throw into code that uses Result defeats the purpose of expressing error handling through types, as it still requires try-catch blocks.
Since Kotlin doesn't have Java's checked exceptions, it's easy to forget to call a try-catch block, which can lead to unhandled errors. To improve this situation, we discussed within the team and decided to standardize our error handling using Kotlin's Result type.
In this article, I'll walk you through how our group writes error handling in practice.
This article does not include the following.
- Explanation of Clean Architecture
- Explanation of Ktor and Exposed
- Comparison between
kotlin-result
and Kotlin's official Result type
Application Directory Structure
Before getting into the main topic, I will explain the directory structure of the application in this group. Below is the well-known diagram of Clean Architecture along with our group's directory structure. As we've adopted Clean Architecture, our application's directory structure is generally organized in line with its principles.
(Source: The Clean Code Blog)
App Route/
├── domain
├── usecase/
│ ├── inputport
│ └── interactor
└── adapter/
├── web/
│ └── controller
└── gateway/
├── db
└── etc
The correspondence between our directory structure and the Clean Architecture diagram is as follows:
domain
directory:entities
usecase
directory:Use Cases
adapter/web/controller
directory:Controllers
adapter/gateway
directory:Gateways
The terminology doesn't match exactly, but basically the domain
directory sits at the core, the usecase
directory surrounds it, and everything under adapter
forms the outermost layer.
Therefore, the allowed direction of dependency is as follows:
usecase
->domain
- Everything under
adapter
->usecase
ordomain
This direction of dependency makes it possible to develop business logic without being affected by factors such as web frameworks or database types.
Error Handling Policy
Basically, our error handling is based on the following policies:
- Use Result type instead of throw in case of processing failure
- When a function returns a Result type, do not use throw
- When returning an exception, use a custom-defined exception type
In the next section, I'll go over each of these policies in more detail, with code examples.
Use the Result type When a Function May Fail
Since Kotlin doesn't have checked exceptions like Java, there's no mechanism to force the caller to handle errors. By using the Result type, you can explicitly indicate that an error may be returned to the caller, reducing the chances of error handling being missed. However, in cases like Result<Unit>
, where the return value isn't used, error handling cannot be enforced unless a custom lint rule is defined. But as of now, we haven't defined one yet.
Code Examples
Below is a simple code example. When defining a function that performs division, it typically results in an error if the denominator is zero. If a function might fail, specify Result as its return type. In this example, Result<Int>
is specified.
fun divide(numerator: Int, denominator: Int) : Result<Int> {
if (denominator == 0) {
return Result.failure(ZeroDenominatorException())
}
return Result.success(numerator/denominator)
}
When Returning an Exception as a Result Type, Wrap the Exception in a Custom-defined Exception
Repositories are defined as interfaces in the domain layer, with their implementations residing in the adapter layer. If a use case layer calls a repository function and handles errors, and the adapter layer returns a third-party library exception as is, then the use case layer must be aware of that third-party exception. In that case, the use case layer becomes dependent on the adapter layer. Here's what that looks like in a diagram:
Interface-based Dependency and Exception-based Dependency (bad example)
To avoid this, we always make sure to wrap any exception returned via Result in a custom-defined exception.
One tricky point when applying Clean Architecture is deciding which layer exceptions belong to, but I personally think it should be in the domain layer. Our group uses a shared set of custom exceptions across multiple services, so we've extracted them into a separate domain library.
Another tricky point with Kotlin's official Result type is that it doesn't allow you to specify the exception type, which means you can't enforce returning only custom exceptions. In cases like this, it may be worth considering the use of kotlin-result
. However, we chose not to adopt it in order to avoid introducing third-party types into the domain code.
Code Examples
Let's say the following interface is defined in the domain layer.
data class Entity(val id: String)
interface EntityRepository {
fun getEntityById(id: String): Result<Entity>
}
Now, consider a case where a third-party library exposes a method like the one shown below, and it's used as-is.
fun thirdPartyMethod(id: String): Entity {
throw ThirdPartyException()
}
Bad Example
If the implementation in the adapter layer returns the exception from the third-party library directly. As shown below, this causes it to leak into the caller such as UseCase
.
class EntityRepositoryImpl : EntityRepository {
override fun getEntityById(id: String): Result<Entity> {
return kotlin.runCatching {
thirdPartyMethod(id)
} // This returns the third party exception
}
}
Good Example
To prevent third-party exceptions from leaking to the caller, wrap them in a custom-defined exception.
class EntityRepositoryImpl : EntityRepository {
override fun getEntityById(id: String): Result<Entity> {
return kotlin.runCatching {
thirdPartyMethod(id)
}.onFailure { cause ->
// wrap with our own exception
CustomUnexpectedException(cause)
}
}
}
throw
in Functions that Return Result
Avoid Using If a function returns a Result type or throws an exception, the caller must handle both. Even if the function author thinks a particular exception doesn't need to be handled by the caller, there may be cases where the caller wants to handle it. For this reason, we have standardized on using the Result type and avoid throwing exceptions explicitly.
In some cases such as database connection errors, it might seem acceptable to throw an exception from the adapter layer and let it propagate directly to the API response, since recovery at the use case level is impossible. However, for issues like failure to update the database, we may still want to log inconsistencies with third-party SaaS. In that case, if we scope out using throw, there's a risk that alerts won't be triggered appropriately.
We believe it's up to the caller to decide whether error handling is necessary, so even if the function author considers it unnecessary, the exception is returned using a Result.
Code Examples
Let's take the save function of a repository as an example. The save function receives the entity class and returns the result as a Result<Entity>
.
Examples of what not to do
As shown below, assume that a connection error was thrown, while other errors are returned using the Result type.
class EntityRepository(val db: Database) {
fun saveEntity(entity: Entity): Result<Entity> {
try {
db.connect()
db.save(entity)
} catch (e: ConnectionException) {
// return result instead
throw OurConnectionException(e)
} catch (e: throwable) {
return Result.failure(OurUnexpectedException(e))
}
return Result.success(entity)
}
}
Now, suppose the use case layer wants to take some kind of action if an error occurs during the save operation. In this case, you must use runCatching
(which internally uses try-catch to convert to Result type).
class UseCase(val repo: EntityRepository) {
fun createNewEntity(): Result<Entity> {
val entity = Entity.new()
return runCatching { // need this runCatching in order to catch an exception
repo.saveEntity(entity).getOrThrow()
}.onFailure {
// some error handling here
}
}
}
Good Example
In the good example, all exceptions are wrapped in a custom-defined exception and returned using the Result type. This allows the caller to remove runCatching
, simplifying the code.
class EntityRepository(val db: Database) {
fun saveEntity(entity: Entity): Result<Entity> {
try {
db.connect()
db.save(entity)
} catch (e: ConnectionException) {
return Result.failure(OurConnectionException(e))
} catch (e: Exception) {
return Result.failure(OurUnexpectedException(e))
}
return Result.success(entity)
}
}
class UseCase(val repo: EntityRepository) {
fun createNewEntity(): Result<Entity> {
val entity = Entity.new()
return repo.saveEntity(entity).onFailure {
// some error handling here
}
}
}
Useful Custom Function for Using the Result Type
andThen
When using a Result type, there are often times when you want to use that value of a successful Result to return a different Result type. For example, updating the status of a specific entity might look like this:
fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> {
val entity = repository.fetchEntityById(id).getOrElse {
return Result.failure(it)
}
val updatedEntity = entity.updateStatus().getOrElse {
return Result.failure(it)
}
return repository.save(updatedEntity)
}
In such cases, writing code becomes easier when the operations can be connected by a method chain. While the kotlin-result
provides the andThen
function for this purpose, Kotlin's official Result type does not. Therefore, our group defined and uses the following method:
inline fun <T, R> Result<T>.andThen(transform: (T) -> Result<R>): Result<R> {
if (this.isSuccess) {
return transform(getOrThrow())
}
return Result.failure(exceptionOrNull()!!)
}
By using this, the previous example can be rewritten as shown below. The result is a bit cleaner, with less repetitive code.
fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> {
return repository.fetchEntityById(id).andThen { entity ->
entity.updateStatus()
}.andThen { updatedEntity ->
repository.save(updatedEntity)
}
}
doInTransaction
for Exposed
Our group uses Exposed as the ORMapper. With Exposed, all database operations must be written within the lambda scope called transaction
. If an exception is thrown within this transaction
scope, it automatically performs a rollback. Since using the Result type avoids throwing exceptions, we created a function that performs a rollback automatically when the Result indicates failure.
fun <T> doInTransaction(db: Database? = null, f: () -> Result<T>): Result<T> {
return transaction(db) {
f().onFailure {
rollback()
}.onSuccess {
commit()
}
}
}
Applying this to the previous example of UseCaseImpl
, it can be used as follows.
fun UseCaseImpl.updataStatus(id: Id) : Result<Entity> {
return doInTransaction {
repository.fetchEntityByIdForUpdate(id).andThen { entity ->
entity.updateStatus()
}.andThen { updatedEntity ->
repository.save(updatedEntity)
}
}
}
respondResult
for Ktor
Our group uses Ktor as the web framework. A function called respondResult
was created to allow Result types from use cases to be returned directly as HTTP responses.
suspend inline fun <reified T : Any> ApplicationCall.respondResult(code: HttpStatusCode, result: Result<T?>) {
result.onSuccess {
when (it) {
null, is Unit -> respond(code)
else -> respond(code, it)
}
}.onFailure {
// defined below
respondError(it)
}
}
suspend fun ApplicationCall.respondError(error: Throwable) {
val response = error.toErrorResponse()
val json = serializer.adapter(response.javaClass).toJson(response)
logger.error(json, error)
respondText(
text = json,
contentType = ContentType.Application.Json,
status = e.errType.toHttpStatusCode(),
)
}
Although it's simple, using this function eliminates the need to call Result.getOrThrow
, making the code a bit cleaner.
fun Route.route(useCase: UseCase) {
val result = useCase.run()
call.respondResult(HttpStatusCode.OK, result.map {it.toViewModel()} )
}
By the way, respondError
is a function that returns an error response from the throwable. We use this function to handle exceptions thrown in the Ktor pipeline and return appropriate responses. We've also created a custom Ktor plugin to handle exceptions.
val ErrorHandler = createApplicationPlugin("ErrorHandler") {
on(CallFailed) { call, cause ->
call.respondError(cause)
}
}
Conclusion
I introduced how our group handles errors, along with some helpful custom functions for the Result type. From what I've seen in various tech blogs, many companies seem to use kotlin-result
, while there's relatively little information out there on using Kotlin's official Result type. We've found Kotlin's official Result type to be sufficient for error handling, so we encourage you to give it a try!
関連記事 | Related Posts
![Cover Image for [Server-side Kotlin] Using Kotlin’s Built-in Result Type for Error Management](/assets/common/thumbnail_default_×2.png)
[Server-side Kotlin] Using Kotlin’s Built-in Result Type for Error Management

UdemyでCoroutinesとFlowの講座を受けました

Kotlin / Ktorで作るクラウドネイティブなマイクロサービス(オブザーバビリティ編)

Structured Concurrency with Kotlin coroutines

Aurora MySQL でレコードが存在するのに SELECT すると Empty set が返ってくる事象を調査した話

バックエンドエンジニアたちが複数のFlutterアプリを並行開発していく中で見つけたベストプラクティス
We are hiring!
【フロントエンドエンジニア(リードクラス)】プロジェクト推進G/東京
配属グループについて▶新サービス開発部 プロジェクト推進グループ 中古車サブスク開発チームTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 中古車 』のWebサイトの開発、運用を中心に、その他サービスの開発、運用も行っています。
【DBRE】DBRE G/東京・名古屋・大阪・福岡
DBREグループについてKINTO テクノロジーズにおける DBRE は横断組織です。自分たちのアウトプットがビジネスに反映されることによって価値提供されます。