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:
- Returning
null - Returning
ApiResult.Failure - Returning
ApiResult.Successwith anullvalue - Returning
ApiResult.Successwith aUserListQueryResponse.Failurevalue - Returning
ApiResult.Successwith aUserListQueryResponse.Successvalue where theuserModelisnull
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