KINTO Tech Blog
Kotlin

[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

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.

Clean Architecture (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 or domain

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)
    }
  }
}

Avoid Using throw in Functions that Return Result

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!

Facebook

関連記事 | Related Posts

We are hiring!

【フロントエンドエンジニア(リードクラス)】プロジェクト推進G/東京

配属グループについて▶新サービス開発部 プロジェクト推進グループ 中古車サブスク開発チームTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 中古車 』のWebサイトの開発、運用を中心に、その他サービスの開発、運用も行っています。

【DBRE】DBRE G/東京・名古屋・大阪・福岡

DBREグループについてKINTO テクノロジーズにおける DBRE は横断組織です。自分たちのアウトプットがビジネスに反映されることによって価値提供されます。

イベント情報