Handling errors in Ktor project

A bunch of scrabble letters made of wood and using black ink for the text. Four letters in the center right spell the word fail

I’ve always been pretty adament that handling errors in your projects should not be an afterthought. This hasn’t changed, so I wanted to write a little post about error handling in your Ktor projects.

Handling Kotlin Exceptions

The most straightforward way to handle errors in your project is to use the “Status Page” plugin, as described in the documentation. Once you’ve installed the plugin, the configuration block lets you register a handler for specific types of exception. In that handler, you have access to the cause of the error and the instance of ApplicationCall, which allows you to sends a response to the client, like so:

install(StatusPages) {
    exception<Throwable> { call, cause ->
        call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError)
    }
}

This example is not the best, because it uses an instance of Throwable, which is the base type for any kind of exception in Kotlin. The error message is also not great, because it just calls toString() on the thrown exception and presents that to the user. Ideally, you’d probably want to print a specific error message for the user and steps for recovery.

Note: the order in which you register your handler does not matter. Under the hood, the plugin keeps a map of exception types and their handler.

One pattern that I like (and that I stole from Vapor) is having a specific kind of error that easily translates to status codes. So I have something like this in my projects:

sealed class AbortExceptionCode(val statusCode: HttpStatusCode) {
    data object BadRequest: AbortExceptionCode(statusCode = HttpStatusCode.BadRequest)
    data object Conflict: AbortExceptionCode(statusCode = HttpStatusCode.Conflict)
    data object NotFound: AbortExceptionCode(statusCode = HttpStatusCode.NotFound)
    data object InternalError: AbortExceptionCode(statusCode = HttpStatusCode.InternalServerError)
    data object Unauthorized: AbortExceptionCode(statusCode = HttpStatusCode.Unauthorized)
    data object NotImplemented: AbortExceptionCode(statusCode = HttpStatusCode.NotImplemented)
}

class AbortException(val code: AbortExceptionCode, val details: String): Throwable()

In an API service, I can throw an AbortException when a resource is not found for example. I can then easily map that to a response in the expected format in the error handler, like so:

install(StatusPages) {
    exception<AbortException> { call, cause ->
        val response = ErrorResponse(message = cause.details)
        call.response.status(cause.code.statusCode)
        call.respond(response)
    }
}

Note: ErrorResponse is a general purpose error that gets serialized into JSON automatically in my API.

Transforming responses with specific HTTP Status Codes

The StatusPage plugin configuration also accepts HTTP status codes instead of exceptions. By default, when you’re using the authorization plugin and a client does not have access to a specific resource, ktor will not throw an exception. Instead, it will send a response with a HTTP 401 with specific headers, and nothing more.

This means that you need to handle those outgoing status codes if you want to present an error to the client. Here is a quick example, using the HTML DSL (that I like):

install(StatusPages) {
    status(HttpStatusCode.Unauthorized) { call, _ ->
        call.respondHtmlTemplate(ErrorTemplate()) {
            title { +"This is not the page you're looking for"}

            content {
                a(href = "/") { +"Go Back" }
            }
        }
    }
}

Conclusion

I’m not sure I love having to handle status codes and exceptions separately, but it’s most likely because I like the pattern in Vapor that I mentioned earlier. Since I deal with various services, I have centralized the error handling so it’s not a real issue. For API services that return serialized content, I transform instances of AbortException into the expected format. For services that serve HTML, I use a common template.

As I’m writing this, I realize that this blog doesn’t really have a proper error page. If you were wondering, the irony of this situation is not lost on me.