I'm implementing a custom Kotlin CoroutineScope that deals with receiving, handling and responding to messages over a WebSocket connection. The scope's lifecycle is tied to the WebSocket session, so it's active as long as the WebSocket is open. As part of the coroutine scope's context, I've installed a custom exception handler that will close the WebSocket session if there's an unhandled error. It's something like this:
val handler = CoroutineExceptionHandler { _, exception ->
log.error("Closing WebSocket session due to an unhandled error", exception)
session.close(POLICY_VIOLATION)
}
I was surprised to find that the exception handler doesn't just receive exceptions, but is actually invoked for all unhandled throwables, including subtypes of Error
. I'm not sure what I should do with these, since I know from the Java API documentation for Error
that "an Error
[...] indicates serious problems that a reasonable application should not try to catch".
One particular situation that I ran into recently was an OutOfMemoryError
due to the amount of data being handled for a session. The OutOfMemoryError
was received by my CoroutineExceptionHandler
, meaning it was logged and the WebSocket session was closed, but the application continued running. That makes me uncomfortable, because I know that an OutOfMemoryError
can be thrown at any point during code execution and as a result can leave the application in a irrecoverable state.
My first question is this: why does the Kotlin API choose to pass these errors to the CoroutineExceptionHandler
for me, the programmer, to handle?
And my second question, following directly from that, is: what is the appropriate way for me to handle it? I can think of at least three options:
- Continue to do what I'm doing now, which is to close the WebSocket session where the error was raised and hope that rest of the application can recover. As I said, that makes me uncomfortable, particularly when I read answers like this one, in response to a question about catching
OutOfMemoryError
in Java, which recommends strongly against trying to recover from such errors. - Re-throw the error, letting it propagate to the thread. That's what I would normally do in any other situation where I encounter an
Error
in normal (or framework) code, on the basis that it will eventually cause the JVM to crash. In my coroutine scope, though, (as with multithreading in general), that's not an option. Re-throwing the exception just ends up sending it to the thread'sUncaughtExceptionHandler
, which doesn't do anything with it. - Initiate a full shutdown of the application. Stopping the application feels like the safest thing to do, but I'd like to make sure I fully understand the implications. Is there any mechanism for a coroutine to propagate a fatal error to the rest of the application, or would I need to code that capability myself? Is propagation of 'application-fatal' errors something the Kotlin coroutines API designers have considered, or might consider in a future release? How do other multithreading models typically handle these kinds of errors?