Should I handle exception with Closable.use{...} in Kotlin?

Viewed 2170

According to the source of Closable.use, if an error occurs, an exception will be thrown.

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }

In most examples of Closable.use, try-catch is not used as shown below. Why isn't error handling needed? Is it safe?

    BufferedReader(FileReader("test.file")).use { return it.readLine() }
2 Answers

We see from Kotlin documentation what is the purpose of the use function:

Executes the given block function on this resource and then closes it down correctly whether an exception is thrown or not.

This function closes the resource properly if the block function completed successfully or threw an exception. It is your responsibility to handle the result of the block function.

If an exception was thrown and there is a way to handle it and proceed with code execution, use a try/catch. If there is nothing to do about it and control should be passed to the caller, it is not necessary to use a try/catch.

This line

 BufferedReader(FileReader("test.file")).use { return it.readLine() }

is not safe. Reading and closing the reader can both throw IOExceptions, which are not RuntimeExceptions (caused by programming errors). That means leaving them uncaught exposes your app to crashing from things outside your control.

Since Kotlin doesn't have checked exceptions, the compiler won't warn you about this. To do this safely, you need to wrap it in try/catch. And if you want to handle read errors differently than close errors, you either need to have inner and outer try/catch statements:

try { 
    BufferedReader(FileReader("test.file")).use { 
        try {
            return it.readLine()
        catch (e: IOException) {
            println("Failed to read line")
        }
    }
} catch (e: IOException) {
    println("Failed to close reader")
}

or wrap the whole thing and extract any suppressed exceptions, but then its cumbersome to distinguish between them:

try {
    BufferedReader(FileReader("test.file")).use { return it.readLine() }
} catch (e: IOException) {
    val throwables = listOf(e, *e.suppressed)
    for (throwable in throwables)
        println(throwable.message)
}

But in practice, you're probably not going to react differently to various IOExceptions, so you can just put the one try/catch outside.

Related