My understanding of error handling

Based on the Algebraic-data type, we can define a type that represents either success with a value or failure with a reason. It's often called Result. In Rust, this is a standard way to handle the error. In Kotlin and TypeScript, the languages provide an exception mechanism as the common way to handle the error, and there are third-party libraries that provide the Result type. In this article, I explain my understanding of error in programming and my ideal way to handle error.

Failure and Exception

The operation fails successfully. ─ originally a joke but it actually makes sense

Suppose we have an API interface that returns the information from the ID. There are two possible errors: entity ID not found, or DB connection timeout. The two errors have a difference: returning “not found” if the ID does not exist is not something wrong. This is what it should do. Sure, the user does not get what the user wants, but it's expected. The service does its best. It's correct behavior to give the user “user not found” if it does not exist. It's more like a result.

However, for the DB connection timeout, something is actually wrong. You should not lose the connection to the DB at any condition. This time, the API does not work as expected.

I don't know if there are official names of these two cases of error. Personally, I call the previous failure and the latter exception. The failure is a result. It's not something you want, but it's not something wrong. The exception means something goes wrong and the operation has to be terminated.

My ideal error handling

In my opinion, the ideal way for error handling is to treat the failure as the return value and the exception as the exception that is thrown. We use a result type to represent either a success value or a failure reason. In Rust, Result is the standard way to handle errors. In Kotlin, I'd like to use result4k or a result library I wrote. Either library provides a Result<T, E> type. You can construct a success result with a value T or a failure with a value E. You can process a Result<T, E> by providing the code path for the success result and g the code path for the failure result. There are some convenient methods to transform the result. In Rust, you can get the success value or return the failure using ?. In result4k we can use result.onFailure { return it }, thanks to Kotlin for allowing us to return the outside function in the lambda expression.

Working with the library

In practice, for a certain programming language, most of the libraries use the same mechanism to produce the error. In Rust, there is no exception[1]. In Kotlin, most of the libraries use only exceptions.

It actually makes sense. Whether an error is a failure or an exception may depend on the context. When opening a file, and the file is not found, is it a failure? It depends. For example, if you named the file by the ID, and you open the file by the ID you try to find, then the file not found is a failure, which means the ID does not exist. If the file has a constant path, then the file not found is an exception. So, for the libraries, it is acceptable to use one mechanism for all the errors.

Usually, in the application code, especially the business logic, it is clear whether an error is a failure or an exception. I apply my ideal handling way when writing these codes.

For Rust which does not have exception, I often have an Exception case in the error enum type. It has an unnamed field which may be an anyhow::Error or a framework error such as actix_web::Error.

Conclusion

Failures and exceptions are different. A failure is more like a result, while an exception is something unexpected. My ideal way to handle error is to use Result type for failure and exception for exception.


  1. Panic-unwind is an anti-pattern in Rust. I find a way to simulate it. But I'm not confident in using it. ↩︎