LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 67: Excessive errors are like insufficient ones

The original article was published on April 17, 2025.

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Excessive errors are like insufficient ones

The following queryUserModel function returns the query result of a user's data model as a return type of ApiResult<UserListQueryResponse?>?. Here, ApiResult and UserListQueryResponse are implemented as sealed interface.

fun queryUserModel(id: UserId): ApiResult<UserListQueryResponse?>? { ... }

sealed interface ApiResult<out T> {
    class Success<T>(val value: T) : ApiResult<T>
    class Failure(val throwable: Throwable) : ApiResult<Nothing>
}

sealed interface UserListQueryResponse {
    class Success(val userModel : UserModel?) : UserListQueryResponse

    class Failure(val errorType: ErrorType) : UserListQueryResponse

    enum class ErrorType { ... }
}

Is there any problem with this code?

Error focus

This code has the issue of having multiple ways to indicate errors or edge cases. For example, there are five possible scenarios for errors or edge cases:

  1. Returning null
  2. Returning ApiResult.Failure
  3. Returning ApiResult.Success with a null value
  4. Returning ApiResult.Success with a UserListQueryResponse.Failure value
  5. Returning ApiResult.Success with a UserListQueryResponse.Success value where the userModel is null

Having multiple ways to indicate errors makes it difficult for the caller to understand what each error means. Even if the error formats differ, the error handling at the caller's end often remains the same, leading to unnecessary code complexity. This is because the errors used for the callee's convenience are directly passed to the caller.

To resolve this, it is better to convert the errors into a representation that is easy and necessary for the caller to use. In many cases, consolidating the error states into a single consistent representation works well. For example, in the following implementation, the distinction between success and failure is represented only by UserModelApiResult, and the error types are consolidated into UserRequestErrorType.

fun queryUserModel(id: UserId): UserModelApiResult {
    // ...
}

sealed interface UserModelApiResult {
    class Success(val userModel: UserModel) : UserModelApiResult
    class Failure(val error: UserRequestErrorType) : UserModelApiResult

    enum class UserRequestErrorType {
        // ...
    }
}

By doing this, the caller only needs to know about UserModelApiResult and UserRequestErrorType to handle errors comprehensively. Of course, if the caller does not need detailed error information, it is acceptable to consolidate errors into null or similar, returning only the information that "an error occurred". (Reference: https://techblog.lycorp.co.jp/en/20231109a)

This approach is particularly important at the boundaries of modules or layers. For example, if there is code connecting to a database or network within the data layer, consider restructuring, abstracting, or removing unnecessary information instead of returning database or network errors directly to the higher layers above the data layer.

In a nutshell

Unify multiple error representations into a single error representation to standardize handling at the caller's end.

Keywords: error, model conversion, abstraction

List of articles on techniques for improving code quality