LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 20: Exceptional exception overwrapping

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 and Exception, changing IOException to another exception will cause it to be caught as Exception, 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 corresponding catch/throws. Only wrap checked exceptions in RuntimeException if recovery is impossible.

Summary

When exceptions occur during exception handling, consider which one should be prioritized.

Keywords: exception, error handling, wrapper

List of articles on improving code quality