This is a pre-SIP for checked exceptions into Scala; i.e, having the ability to make the compiler emit errors when checked exceptions are not handled.
Motivation
A lot has already been said about checked exceptions over the years. There are many reasons for and against checked exceptions.
In Scala we have Try
, Either
and Option
to deal with exceptional situations, but sometimes exceptions could produce clearer code. Some may refute that such code is clearer, or argue that it incurs too much penalty or violates some theoretical principle, but I believe this is a matter of opinion – where there is no clear verdict – and a matter of proper and educated usage.
What I love about Scala is its combination of features and ideas from (seemingly) opposite paradigms, often providing more than one way to model the code. Scala is less opinionated, providing less idiomatic solutions, and lets the developers use the tools of their choice. I view checked exceptions as one such tool, and so I would like to have them in the language.
There has been an attempt to introduce checked exceptions via a compiler plugin, but it is (a) outdated and (b) doesn’t support the full capabilities that exceptions require, which will be described in this proposal.
Goals
The goals of this discussion are:
- Identifying technical problems.
- Suggesting improved designs.
- Expressing opinion regarding specific features.
The following are not the goals of this discussion:
-
Arguing over the motivation behind checked exceptions; the motivation should be discussed only when it’s relevant to a certain technical decision.
-
Planning the introduction of the features in this proposal in the language. It may be worth mentioning technical migration concerns, but I believe that overall we should consider this proposal as a major feature that will require some experimentation, and will likely be introduced with a compiler-flag.
Syntax & Features
The first new addition is the throws
keyword, which replaces the @throws
annotation and behaves just as it does in Java:
def foo(bar: Int): Int throws Exception = ???
def bar(): Unit = {
foo(1) // compiler error (need to handle exception)
}
^ - the exception character
In order to reduce verboseness and keep a method’s signature mostly keyword-free, a caret (^
) may be used instead of throws
:
def foo(bar: Int): Int ^ Exception = ???
The caret is a good candidate for an “exception character”, and for these reasons:
- It’s simple, appearing in the ASCII table and easily available on QWERTY keyboards.
- It’s not already used in Scala as a special character.
- It symbolizes “up”, corresponding to the general notion of exceptions being “thrown up” or “raised”.
Function types
Types of functions that throw exceptions can be expressed with a caret:
type ExceptionalF = Int => Int ^ Exception
def foo(bar: Int): Int ^ Exception = ???
val foo2: ExceptionalF = foo
def bar(exceptional: ExceptionalF) = ???
bar(foo)
Similarly to the return type, a function’s exception type is covariant.
Functions that do not throw any exception can still be assigned to an exceptional function type, as they can be thought of as throwing Nothing
:
val f: ExceptionalF = (i) => i + 1
def baz(i: Int): Int = ???
// is equivalent to
def baz(i: Int): Int ^ Nothing = ???
// and can be assigned to
val f: ExceptionalF = baz
Multiple exceptions
A function that may throw multiple exception types can express this using dotty’s union types:
def foo(bar: Int): Int throws ExceptionA | ExceptionB = ???
def foo(bar: Int): Int ^ ExceptionA | ExceptionB = ???
type ExceptionalF = Int => Int ^ ExceptionA | ExceptionB
Generic exceptions
Since functions are first-class citizens, there is a need to be able to bind exception types to those of other functions. We could use generic types encoding for this purpose:
trait Container[A] {
def map[B, E](f: A => B ^ E): Container[B] ^ E = ???
}
In this example, the exception type of the map
function is determined by the exception type of f
, similarly to how the return type does. This means that map
doesn’t need to handle exceptions thrown by f
.
Anchored exceptions
Checked exceptions that are propagated through a large stack only to be handled at the beginning of it may produce code which is harder to maintain, since every function in that stack is required to declare the exception. This becomes a real burden to refactor such stack when the original exception type changes – all of the functions’ signatures on the stack need to change as well.
One solution to this problem can be attempted via a new mechanism called “anchored exceptions”. The term is borrowed from this article (by Marko van Dooren and Eric Steegmans), which describes the problem at length and suggests a solution with a less-than-optimal syntax, as it is extremely verbose.
I would like to propose a more concise syntax which is also tailored to Scala. This syntax is far from finalized, and I haven’t experimented with all the potential cases, but I believe this is the right direction in coming up with a new and useful syntax.
Here is a simple demonstration of this capability:
def read(path: String): String ^ IOException = ???
def print(path: String): Unit ^ AE = {
val content = read(path) ^ AE
Console.print(content)
}
In this example, the exception type of print
is anchored to that of read
via the AE
identifier, which means two things:
- The exception type of
print
will change along with the exception type ofread
. - The compiler will not emit an error in
print
, as the exception ofread
is declared forprint
as well.
Anchored exception types may also be combined with non-anchored ones:
def print(path: String): Unit ^ AE | ExceptionB = {
if (path.size == 0) throw new ExceptionB
val content = read(path) ^AE
Console.print(content)
}
Or with generic exception types:
def mapAndValidate[E](f: A => B ^ E): B ^ E | ValidationE = {
val b = f(a)
validate(b) ^ ValidationE
b
}
In fact, it may be possible to encode a generic exception propagation using anchored exceptions and the ^=>
operator:
trait Container[A] {
def map[B](f: A ^=> B): Container[B] ^ f = ???
// equivalent to:
def map[B, E](f: A => B ^ E): Container[B] ^ E = ???
}
Currying
Multi parameter lists (currying) support an exception type only after the last list:
// valid:
def foo(bar: Int)(baz: String) ^ Exception = ???
// invalid:
def foo(bar: Int) ^ Exception (baz: String) = ???
The main reasoning for disallowing exception types in the middle of a curried signature is due to the confusing and somewhat irregular syntax it produces.
Custom exceptions
Creating custom exceptions is considered a good practice, but it is rather cumbersome to implement in Scala. It could be useful to introduce a new way of defining custom exceptions that reduces the boilerplate while maintaining these abilities:
- Un-applying the custom exception (extractor object).
- Properly handling construction with
null
message / cause. - Construction with or without a stacktrace.
- Enabling suppression.
Here is one potential way of encoding custom exceptions:
// definition
exception ParseException(line: Int, reason: String) {
def message: String = s"$line: $reason"
override def checked = false
}
// construction (throw is not required)
throw ParseException(22, "invalid character")
throw ParseException(22, "invalid character", cause = e)
throw ParseException(22, "invalid character", trace = false)
// pattern matching
e match {
case ParseException(line, reason, message, cause) => ???
}
There are obviously other potential ways of expressing this and many small variations that could be made to this one; this is just the general idea.
Exception wrapping
Custom exceptions are often used in order to wrap another exception and add more context to the stack trace:
try {
unsafeOperation
} catch {
case NonFatal(e) => throw ParseException(22, "invalid character", cause = e)
}
However, this syntax is quite verbose; perhaps it’s worth introducing a more concise syntax for this use-case:
unsafeOperation ^ e => ParseException(22, "invalid character", cause = e)
I’m not sure whether combining this syntax with the syntax of anchored exceptions is the right choice or not.
Pretty stack traces
One last thing that might be worth adding to the standard SDK is a “prettier” representation of stacktraces – or rather, a more readable one – like in this Scastie example (but obviously more efficient).
Problematic cases
There are a few cases in which it may be problematic to have a checked exceptions mechanism:
- Lazy evaluations (
lazy val
). - Implicit definitions (
implicit def
). - Constructors (body of
class
orobject
). - Top-level code (instead of package objects).
It may be possible to overcome the difficulties in some of these scenarios – I didn’t put too much thought to it – though I suspect that there are some which are unavoidable. In such cases, perhaps it’s best to have the compiler warn on throwing a (checked) exception / invoking a method with a checked exception type.