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.
Exceptional exception overwrapping
Kotlin's Closeable.use
is a handy higher-order function that calls Closeable.close()
after executing its argument. In the code below, inputStream
is closed after the lambda on the second line is executed.
file.inputStream.use { stream ->
// We can use stream here
}
// After `use` execution,
// we don't need to call `close()` for `stream` here.
In languages with scope pointers or automatic reference counting (ARC), you can achieve similar results by releasing resources in destructors. In Java, you can use the try-with-resources statement by implementing AutoCloseable
.
One benefit of this pattern is that it prevents resource leaks even if an exception is thrown or a non-local return occurs. In the code below, an Exception
is thrown inside the lambda of use
, but inputStream.close()
is still called.
file.inputStream.use { stream ->
// We can use stream here
throw Exception()
}
// Even an exception is thrown,
// we can expect `close` was called.
You can implement a similar pattern for custom interfaces or classes. In the Disposable
example below, we expect dispose
to be called when done. By defining an extension function like use
for such custom classes, you can prevent forgetting to call dispose
. (In languages without extension functions, you can achieve the same by using arguments instead of receivers.)
interface Disposable {
fun dispose()
}
fun <T : Disposable?, R> T.use(block: (T) -> R): R {
try {
return block(this)
} finally {
dispose()
}
}
After implementing Disposable
, you might realize that dispose
could also throw an exception. This means that during a use
call, exceptions could occur both during the block
execution and the dispose
call. Both exceptions could be thrown in a single use
call. To handle this, we updated the implementation to wrap exceptions in a DisposableException
.
class DisposableException(
val exceptionAtBlock: Throwable?,
val exceptionAtDispose: Throwable?
): Exception()
interface Disposable {
fun dispose()
}
fun <T : Disposable?, R> T.use(block: (T) -> R): R {
var exceptionAtBlock: Throwable? = null
try {
return block(this)
} catch (originalException: Throwable) {
exceptionAtBlock = originalException
throw DisposableException(exceptionAtBlock, null)
} finally {
try {
this?.dispose()
} catch(exceptionAtDispose: Throwable) {
throw DisposableException(
exceptionAtBlock,
exceptionAtDispose
)
}
}
}
Is there a problem with this code?
Exception within exception
The code above replaces thrown exceptions with another exception. However, this conversion might not be what the caller expects.
For example, in the SomeDataWriter
below, an IOException
might occur during write
. The caller someFunction
intends to catch that exception, but since the thrown exception is DisposableException
, the catch
won't work as intended. In this example, it's relatively easy to notice the mistake because both exception handling and use
are in someFunction
. However, if you extract the logic into a helper function, the mistake might be harder to spot.
class SomeDataWriter : Disposable {
fun write(someData: SomeData) {
// write someData
if (/* for some error case */) {
throw IOException(...)
}
}
fun dispose() { /* ... */ }
}
fun someFunction(...) {
try {
createWriter()
.use { writer -> writer.write(someData) }
} catch (exception: IOException) {
// handle IO Exception
}
}
Eliminate unnecessary wrapping
When multiple exceptions can occur in one place, it's better to use Throwable.addSuppressed
to clarify which exception is more important rather than wrapping them in another exception. However, you need to carefully decide which exception is more important. The following code is an inappropriate implementation.
fun <T : Disposable?, R> T.use(block: (T) -> R): R {
var exceptionAtBlock: Throwable? = null
try {
return block(this)
} catch (originalException: Throwable) {
exceptionAtBlock = originalException
throw originalException
} finally {
try {
this?.dispose()
} catch (exceptionAtDispose: Throwable) {
if (exceptionAtBlock != null) {
exceptionAtDispose.addSuppressed(exceptionAtBlock)
}
throw exceptionAtDispose
}
}
}
In this implementation, if an IOException
occurs in block
and another exception exceptionAtDispose
occurs in dispose
, the final thrown exception will be exceptionAtDispose
. Since dispose
is a supplementary call, prioritizing the exception in block
results in less confusion.
The corrected code is as follows. In this code, if exceptions occur in both block
and dispose
, the one from block
is prioritized. (The implementation of Closeable.use
in Kotlin 1.1 and later behaves similarly. For more details, refer to Closeable?.closeFinally
.)
fun <T : Disposable?, R> T.use(block: (T) -> R): R {
var exceptionAtBlock: Throwable? = null
try {
return block(this)
} catch (originalException: Throwable) {
exceptionAtBlock = originalException
throw originalException
} finally {
try {
this?.dispose()
} catch (exceptionAtDispose: Throwable) {
if (exceptionAtBlock == null) {
throw exceptionAtDispose
} else {
exceptionAtBlock.addSuppressed(exceptionAtDispose)
}
}
}
}
Java an exception?
In languages like Java with checked exceptions, wrapping exceptions in another exception is relatively safe because you can distinguish exceptions by their types. However, be cautious in the following cases:
- If the caller handles multiple types of exceptions and there is a parent-child relationship between them: For example, if you catch
IOException
andException
, changingIOException
to another exception will cause it to be caught asException
, and this won't be a compile-time error. - If you convert to an unchecked exception: Wrapping a checked exception in
RuntimeException
allows the code to compile without correspondingcatch
/throws
. Only wrap checked exceptions inRuntimeException
if recovery is impossible.
Summary
When exceptions occur during exception handling, consider which one should be prioritized.
Keywords: exception
, error handling
, wrapper