Checked Exceptions
Introduction
Scala enables checked exceptions through a language import. Here is an example, taken from the safer exceptions page, and also described in a paper presented at the 2021 Scala Symposium.
import language.experimental.saferExceptions
class LimitExceeded extends Exception
val limit = 10e+10
def f(x: Double): Double throws LimitExceeded =
if x < limit then x * x else throw LimitExceeded()
The new throws
clause expands into an implicit parameter that provides a CanThrow
capability. Hence, function f
could equivalently be written like this:
def f(x: Double)(using CanThrow[LimitExceeded]): Double = ...
If the implicit parameter is missing, an error is reported. For instance, the function definition
def g(x: Double): Double =
if x < limit then x * x else throw LimitExceeded()
is rejected with this error message:
| if x < limit then x * x else throw LimitExceeded()
| ^^^^^^^^^^^^^^^^^^^^^
|The capability to throw exception LimitExceeded is missing.
|The capability can be provided by one of the following:
| - Adding a using clause `(using CanThrow[LimitExceeded])` to the definition of the enclosing method
| - Adding `throws LimitExceeded` clause after the result type of the enclosing method
| - Wrapping this piece of code with a `try` block that catches LimitExceeded
CanThrow
capabilities are required by throw
expressions and are created by try
expressions. For instance, the expression
try xs.map(f).sum
catch case ex: LimitExceeded => -1
would be expanded by the compiler to something like the following:
try
erased given ctl: CanThrow[LimitExceeded] = compiletime.erasedValue
xs.map(f).sum
catch case ex: LimitExceeded => -1
(The ctl
capability is only used for type checking but need not show up in the generated code, so it can be declared as erased.)
As with other capability based schemes, one needs to guard against capabilities that are captured in results. For instance, here is a problematic use case:
def escaped(xs: Double*): (() => Double) throws LimitExceeded =
try () => xs.map(f).sum
catch case ex: LimitExceeded => () => -1
val crasher = escaped(1, 2, 10e+11)
crasher()
This code needs to be rejected since otherwise the call to crasher()
would cause an unhandled LimitExceeded
exception to be thrown.
Under the language import language.experimental.captureChecking
, the code is indeed rejected
To integrate exception and capture checking, only two changes are needed:
CanThrow
is declared as a class extendingControl
, so all references toCanThrow
instances are tracked.- Escape checking is extended to
try
expressions. The result type of atry
is not allowed to capture capabilities defined in the body of thetry
.