- Type: Design proposal
- Author: Ben Leggiero
- Status: Under consideration
- Revision: 2
- Discussion: KEEP PR #81
The purpose of Kotlin, Swift, and other such modern languages, as I see them, are to enhance the programmer's experience of writing code (to bring delight and joy back into the programming experience); to do more with less and produce higher-quality output (that is, bytecode, assembly, JS, or whathaveyou). If that's true, then the languages themselves should make it obvious when you're writing something dangerous that is likely to crash your app (which makes programming very frustrating). This proposal attempts to incorporate exception handling into the Kotlin language in a way that achieves that goal.
- Original proposal
- Added
rethrows
for functions that take in a closure that might throw
My program crashing without me knowing whether it will at compile-time is something I hate; it makes the programming experience worse, not better. Often, I have designed my end of the code without hese crashes, but because the person on the SDK side might have decided their code can throw (very common when using Java SDKs), and the people on the language side with Kotlin decided the compiler will ignore exceptions by default, my program will crash through no obvious fault of my own, because the compiler couldn't warn me.
This is why we have optionals that must be unwrapped before using them, as opposed to making NullPointerExceptions
the default like Java. On that same note, this is why the old throw-catch pattern was invented in the first place: to avoid the mysterious C-style signal-based crashes. It feels like a huge step back to just let all exceptions through by default.
This is inspired by Swift's approach to error handling.
I propose Kotlin adopt a similar pattern to Swift's approach to exceptions, so that all the following are a part of the language:
A function's signature may include the throws
keyword to indicate that it throws an exception. The type of the exception is not indicated in the function signature. Here, "``: Int" still specifies that the function returns an
Int`, but has no bearing on the exception type.
fun danger(i: Int) throws : Int {
if (Random().nextBoolean()) {
throw UnsupportedOperationException()
} else {
return i
}
}
If a function does throw an exception, or does not handle an exception thrown inside it, then it must be declared using the
throws
keyword; omitting this would be a compiler error.
Similar to Declaration Site A, but for closures:
typealias Danger = (Int) throws -> Int
Note that a closure like this does not neccessarily have to throw. This is elaborated upon in Call Sites G and H.
For a function that takes closure arguments which might throw an exception (for instance, Declaration Site B's Danger
), the function may be declared using the rethrows
keyword, meaning that if the closure throws, whatever calls the rethrowing function needs to handle that, but if the closure doesn't throw, the caller does not need to handle an exception:
fun rethrower(danger: Danger) rethrows : Int {
return try danger()
}
Note that because
danger
is of typeDanger
, which is a closure that may or may not throw,rethrower
is allowed to be declared asrethrows
instead ofthrows
. It may also be declared asthrows
, in which case it must be treated just like Declaration Site A, even when the given closuredanger
doesn't throw. It is a compile-time error for rethrower to be declared withoutthrows
orrethrows
.
Generally, when calling a function, it must be obvious whether it might throw an exception, so safe ones are called normally, but dangerous ones (which might throw an exception) must be preceded by a form of try
; the compiler would emit an error if either of these is disobeyed (try
preceding a safe function, or not preceding a dangerous one). This is elaborated upon below:
Use a do
-catch
block-style paradigm to catch anything. The type and value of the exception are discarded. Note that there still cannot be a naked do
block; it must be followed by catch
to catch exceptions or while
to create a loop.
do {
println("This line always runs")
val a = try danger(1)
println("This line is only run if `danger` does not throw")
} catch {
println("I caught, but I don't care what I caught")
}
println("This line always runs")
Use a do
-catch
block-style paradigm to catch anything. The type of the exception is not restricted, and the value is saved in a argument, whose type can be later checked.
do {
println("This line always runs")
val b = try danger(2)
println("This line is only run if `danger` does not throw")
} catch (x: Throwable) {
println("I caught $x, but I don't care what type it is")
}
println("This line always runs")
Use a do
-catch
block-style paradigm to catch only a specific type of exception, with the option to catch further specific types andor have a catch-all that ignores unexpected types. The first block whose type matches the thrown exception is the only one called. This is the most like Java's approach.
do {
println("This line always runs")
val c = try danger(3)
println("This line is only run if `danger` does not throw")
} catch (x: UnsupportedOperationException) {
println("I caught $x, as an unsupported operation")
} catch (x: NumberFormatException) {
println("I caught $x, as a number format exception")
} catch (x: Throwable) { // Note: "} catch {" is also legal here
println("I caught $x, but I don't care what type it is")
}
println("This line always runs")
Important: If no
catch
block matches the type of thrown exception, then the program will halt/crash. It's recommended to ensure aThrowable
is caught (or the Call Site B-style catch-all is used) after all explicit exception types are caught.
Use a single-line "optional-try
" paradigm to catch any exception. The type and value of the exception are discarded, and the function's result is wrapped in an optional which is null
if the function throws, but contains a value if the function does not throw.
This essentially behaves as if the function's return type was optional.
println("This line always runs")
val d = try? danger(4) // d is Int?
println("This line always runs, even if danger throws")
Note that even if the return type is
Unit
,try?
can be used; its return value can be ignored just as always
Use a single-line "force-try
" paradigm to ignore all exceptions. The type and value of the exception are discarded. If the function throws an exception at run-time, the variable is never set and the application crashes/halts. Else, it is the output as a non-wrapped type.
This essentially behaves like Kotlin today would treat val e = danger(5)
when used outside a try
-catch
block.
println("This line always runs")
val e = try!! danger(5) // e is Int
println("This line is only run if danger does not throw")
Place the call site inside a new function that is marked with throws
. The possibly-throwing function(s) called inside it must still be marked with try
, but exceptions don't need to be handled inside the new function, itself. This allows handling to be propagated up the chain to the caller of the new function.
fun useDanger() throws {
println("This line always runs")
val f = try danger(6)
println("This line is only run if `danger` does not throw")
}
Note: It is a compiler error for a function like this to omit the
throws
keyword
Calling a function that is declared as rethrows
inherits the throwing state of the closure passed to it. This means that any closure declared like this with an unhandled exception is implicitly declared as throws
:
val g1 = rethrower { 42 } // `try` is not allowed; given closure can not throw
val g2 = try rethrower { try danger(7) } // `try` is required in both contexts; given closure might throw, but it's not handled in the closure
val g3 = rethrower { // `try` not allowed on this line because the given closure catches all its own exceptions
do {
try danger(7) // `try` is required here as it always is
} catch {
print("An exception was caught, but I don't care what it was")
}
}
Calling a function that is declared as rethrows
by passing it a function that is declared with throws
, rather than declaring a new closure at the call site like Call Sites G, means that try
is required, since the safety of the given function cannot be determined at compile time. Conversely, passing a function that is declared without throws
is assumed safe and does not allow try
:
val h1 = try rethrower(::danger) // `try` is required
fun safe(i: Int) : Int { return i }
val h2 = rethrower(::safe) // `try` must be omitted
- I propose that all 8 of the above ways to handle exceptions (Call Sites A-H) should be built into the language, without the aid of annotations andor libraries.
- I propose that a compile-time error should occur if a function or closure which marked with
throws
is called without using one of the 8 above ways of handling said exception. - I propose that a compile-time error should occur if a function or closure containing a
throw
or unhandledtry
statement does not havethrows
(orrethrows
if that exception might be thrown by a closure) in its declaration.
These should allow for fully-featured exception handling, and should provide peace-of-mind to the programmer knowing all exceptions are handled, without introducing the unruly clutter that Java does.
throws
and rethrows
would be introduced, so there is the potential for it to break existing source code; any programmer-defined symbols named throws
or rethrows
would then fail to be recognized as symbols. However, since these can only appear in a function or closure typealias
declaration, they could be treated as a regular token in all other contexts.
If that is too difficult (or impossible) to do with the current compiler, then all current code that uses the token throws
or rethrows
will fail to compile.
The usage of the try keyword would have to be changed so that it is placed immediately before calling a function that throws, rather than at the start of a block like Java and current Kotlin.
The usage of the do
keyword would have to be changed so that its block can be followed by a catch
. This should be done in such a way that it does not affect do { ... } while
loops.
- Cause the stdib
@Throws
parameter to emit compile-time errors (or warnings) when functions using it are called outside atry
-catch
block, possibly typechecking against its arguments if any are provided. This would be the least-invasive change and provide peace-of-mind that thrown exceptions are caught, but none of the callsite benefits of the above proposals, such as creating an optional withtry?
. - Implement the same solution currently used in Java. This would be a familiar syntax to anyone moving from Java to Kotlin, but comes with heavily-typed baggage. This also has the same callsite downsides as alternative #1.
- Only implement the
throws
andrethrows
keywords and leave the callsite handling the same, like alternative #1. This would be the least-invasive language change, but has all the disadvantages of alternative #1.
Note that I use "exception" and "error" interchangeably here. I know that errors aren't technically exceptions, but I like this phrasing better than "Throwable". Assume I always mean "Throwable" wherever I say "exception" or "error".