Kotlin library that provides a PublicException type: an exception with a message that is meant to
be exposed to users.
When using this with liflig-http4k-setup,
any PublicException thrown in the context of an HTTP handler will be caught by a
PublicExceptionFilter, and mapped to an HTTP response.
Contents:
We distinguish between handling and reporting errors on servers, defining them as follows:
- Error handling is when your application encounters an error that it knows about and can recover from there-and-then
- Error reporting is when your application encounters an error that it cannot solve by itself. It
must then report it, both to:
- The user, by returning a response signaling that something failed
- The developer, by logging the error, ideally with as much context as possible for debugging
This library aims to improve error reporting.
The base case for error reporting of exceptions in Kotlin/Java HTTP servers typically looks like this:
- A 500 Internal Server Error is returned to the user, with no details about the specific exception
- The exception is logged, with a stack trace
This is a pretty good base case:
- The user is alerted that something failed, but no potentially sensitive information is exposed
- The developer gets valuable debugging context from the stack trace
When trying to improve error reporting, we want to build on this base case, without taking away from it.
One thing that you may want to do to improve on this base case, is to return a more specific response to the user (perhaps a different HTTP status code, and a response body describing more details about the error, so the user may solve it themselves). This is easy enough to do when the error occurs in your HTTP handler. But often, errors occur further down the call stack, and then we must deal with how to propagate the error up to the HTTP handler, with enough context to return our intended response to the user.
This is where Kotlin developers may reach for something like
Arrow. Using Arrow, we'd change our functions to return an
Either<Error, Success> type, and then we'd check the Error variant in our HTTP handler, mapping
it to an appropriate error response. This is fine on paper, but in practice, using Arrow may take
away from the other aspect of error reporting: including as much context as possible when logging
the error for the developer. This is because:
- You'll typically define custom error types when using Arrow. If the error originally was caused
by an exception, you must remember to attach the exception to your error type, and propagate
that up to your HTTP handler - and then, you must remember to log that exception, so that its
stack trace is included.
- In practice, it's easy to forget to attach exceptions to your custom error types. Then, when an error occurs in production, we're left without a stack trace of the underlying error, losing valuable context.
- If there is more than 1 level between the original error and your HTTP handler, then you must
wrap underlying error variants as you propagate errors up the stack.
- Let's say that our HTTP handler calls
Service1, which again callsService2. Both services define sealed classes with error variants they may return,Service1ErrorandService2Error. If we encounter an error inService2, which we want to propagate up to our HTTP handler, thenService1Errormust attach theService2Error, or else we lose context about the root cause.
- Let's say that our HTTP handler calls
These issues may sound banal, but we have seen them happen again and again when using Arrow.
Fundamentally, this comes down to using the wrong tool for the job: Arrow is a great tool for
explicit error handling, but is not ideal for error reporting. Exceptions are quite good for
error reporting, since they automatically propagate up the stack, and keep their original context
(the stack trace). And they have a built-in way of attaching more context, by wrapping an exception
in a new exception and setting the original exception as the cause. When using Arrow, we have to
recreate these mechanisms, by manually propagating errors up the stack, and attaching context about
the original error as fields on our custom error types. But when doing this manually, it's easy to
lose context on the way.
So: we want to use exceptions for error reporting, so that we don't lose context when logging the
error for the developer. But then how do we return a more specific error response to the user?
That's where PublicException fits in: it gives you the benefits of exceptions (stack
trace, automatic propagation up the stack), but lets you set a status code and user-facing message,
to give a more useful error response to the user. When using
liflig-http4k-setup, the default server
middleware will catch PublicExceptions, automatically mapping them to appropriate HTTP responses.
PublicException is a pragmatic solution to serve both aspects of error reporting: keeping as much
context as possible for the developer, while also allowing you to give a more useful response to the
user. Note that it is not the appropriate tool for error handling - if a part of your application
deals with well-known, recoverable errors, you may want to use something like Arrow instead for
that. But when all you want to do is to give the user more context about an error, without the risk
of losing internal context for the developer, then PublicException may be the right tool.
Maven: We recommend adding liflig-public-exception to the dependencyManagement section, so
that the same version is used across Liflig libraries. It's good practice to pair this with the
Maven Enforcer Plugin, with the
<dependencyConvergence/>
and
<requireUpperBoundDeps/>
rules.
<dependencyManagement>
<dependency>
<groupId>no.liflig</groupId>
<artifactId>liflig-public-exception</artifactId>
<version>${liflig-public-exception.version}</version>
</dependency>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>no.liflig</groupId>
<artifactId>liflig-public-exception</artifactId>
</dependency>
</dependencies>mvn clean installmvn spotless:checkmvn spotless:apply