Pattern matching and exception handling unification

Recently I came across this JEP:
JEP draft: Exception handling in switch (Preview)

I wonder if Scala would consider to catch up?
So that instead of

def fooThatCanThrow(): Int = ???

try
  fooThatCanThrow() match
    case 1 => "one"
    case _ => "other result"
catch
  case ex: SomeSpecificException => "specific error"
  case ex                        => "other error"

we could write something like this

fooThatCanThrow() match
  case 1 => "one"
  case _ => "other result"
  case catch ex: SomeSpecificException => "specific error"
  case catch ex                        => "other error"

which may seem neater.

Or, if we need to cover a code block instead of just one expression, it could look like

{
  a
  bunch
  of
  statements
} match
  case 1 => "one"
  case _ => "other result"
  case catch ex: SomeSpecificException => "specific error"
  case catch ex                        => "other error"

I apologize if it was discussed previously – I didn’t manage to find one.

1 Like

OCaml has something like that as well, don’t see the point why we need to complicate the syntax here. Scala already can do this:

try fooThatCanThrow() match
  case 1 => "one"
  case _ => "other result"
catch
  case ex: SomeSpecificException => "specific error"
  case ex                        => "other error"

That’s more regular and IMO clearer than the proposal.

7 Likes

I really think, as Martin says, that Scala already does it better than Java’s proposal.

The JEP itself says that it’s syntactically legal but of no functional consequence to mix exception cases and normal cases, and it’s always equivalent to a rewritten form with the exceptions afterwards. And it doesn’t catch exceptions in the pattern match despite being embedded in the try switch block, which is also not necessarily what one would expect. It’s always a dubious language design choice to make function not follow form.

switch (foo()) {
  case Box(x) -> x.bar()
  case throws IllegalArgumentException iae -> "oops"
  case Box2(y) -> baz(y)
  case throws ArrayIndexOutOfBoundsException aob -> "yikes"
}

This code looks like it probably catches an IllegalArgumentException in foo(), x.bar(), or baz(y), and an ArrayIndexOutOfBounds exception in foo() or baz(y) but not in x.bar().

And that’s not what it does at all. It catches the two exceptions for foo() and doesn’t catch anything for x.bar() or baz(y).

You can learn that this is what it means, but the idea that you want to catch exceptions in the matched expression but not in the match items is honestly a bit weird. The Java code above would be written in Scala now, without assigning any temporary variables, as the highly awkward:

boundary:
  {
    try foo()
    catch
      case iae: IllegalArgumentException => boundary.break("oops")
      case aob: ArrayIndexOutOfBoundsException => boundary.break("yikes")
  } match
    case Box(x) => x.bar()
    case Box2(y) => baz(y)

I certainly agree that the case that Java is handling, where you catch the exception only in the block that creates the thing to match, is difficult to express in Scala. Normally in Scala you would package the exception into a result type.

So I guess if you wanted to advocate for the Java-style functionality, you should argue for why this “catch foo() but not the match block” function is something we want to make easy to express yet not with a result type; and then propose syntax that makes this clear.

As an aside, if we did want to do this, I think we would want to insist that catch statements go first, and probably be in their own block, like so:

foo() match
  catch
    case iae: IllegalArgumentException => "oops"
    case aob: ArrayIndexOutOfBoundsException => "yikes"
  case Box(x) => x.bar()
  case Box2(y) => baz(y)

In this case, form follows function. You can read that and without reading the docs get a pretty good guess as to how it works.

But I think this is an error–it is introducing a new syntax to partially handle exceptions as sum types, when what one really should be doing is using a sum type like Either where you have richer functionality.

Addendum: if you really love this functionality, you could write a catchmatch function that would be close:

catchmatch(foo()) {
  case iae: IllegalArgumentException => "oops"
  case aob: ArrayIndexOutOfBoundsException => "yikes"
}{
  case Box(x) => x.bar()
  case Box2(y) => baz(y)
}

so you also need to argue why this should be language syntax and not something a library provides.

Addendum 2: here’s the function. I haven’t checked the bytecode to see if the inlined partial function is compiled into a fully optimized catch statement, but if not, that’s a compiler enhancement that could come later.

inline def catchmatch[A, Z](inline a: => A)(inline handler: PartialFunction[Throwable, Z])(inline f: A => Z) =
  boundary[Z]: outer ?=>
    f(
      boundary[A]: inner ?=>
        boundary.break(
          try boundary.break(a)(using inner)
          catch handler
        )(using outer)
    )

The using statements aren’t strictly necessary, but I’ve included them to clarify the logic. You can use this as a catchmap too, because f doesn’t have to be a pattern match. It’s permitted, though, so this is strictly more powerful than the Java behavior with otherwise identical semantics as far as I can tell.

Addendum 3: One of the things I really love about Scala is that often other languages (e.g. Java) have to create a language enhancement to do what Scala can do as a library. This is one of those times!

1 Like

Just to be clear, my justification for the new approach is not exactly about code indentation only. It is rather about shifting the perception of exceptions from “it is some special mechanism for iterrupting code and propagating to somewhere else” towards “exceptions are nothing but irregular return values”. Let me elaborate a little and provide a short example:

def resForZero = "It's just zero"
def resForNegative = new IllegarArgumentException("It's negative")

def foo(a: Int): String =
  if a == 0 then return resForZero
  if a < 0 then throw resForNegative
  
  otherwise
  compute
  and(return)
  the.result  

Forget about the exact syntax and indentation rules for a minute please and just think about what this code does at first two lines. In fact,

  • the first line interrupts the function with return statement and provides a regular return value which is a part of the function type;
  • the second line interrupts it with some irregular value which is not a part of the function type.

At the end of the function some return value has to be provided eventually. It can be either a regular or irregular one, though.

Regular return values are expected to be handled by a caller on a callsite. Irregular values, on the other hand, may or may not be handled on the callsite – it is up to the caller to handle it on the spot or somewhere later. But otherwise on the high level those are basically just return values.

This consideration is probably one of the reasons why checked exceptions in other languages never make a lot of sense. Because on the logical level checked exception simply introduce an alternative channel for regular return values, but implemented differently under the hood.

Let me elaborate a bit more to explain where I am going with that.

  1. Generally speaking, exceptions don’t have to inherit to any particular superclass. The fact that in Java they do is very JVM-specific. For example, in C++ it is possible to write something like this:

    #include <iostream>
    #include <string>
    #include <cmath>
    
    float foo(float number) {
        if (number < 0.0f)
            // std::string is just a string, not a special exception class
            throw std::string("negative input");
    
        return std::sqrt(number);
    }
    
    void bar(float number) {
        try {
            auto result = foo(number);
            std::cout << "Result: " << result << std::endl;
        } catch (const std::string& err) {
            std::cout << "Error: " << err << std::endl;
        }
    }
    
  2. In Scala exceptions are sometimes used as carriers for regular return values:

    // Scala2
    def foo(a: Option[Int]): String = {
      a.foreach {
        case 0 => return "zero" // throws NonLocalReturnControl
        case aa if aa < 0 => throw new IllegalArgumentException("negative")
        case _ => ()
      }
      "non-zero or empty"
    }
    
    // Scala3
    import scala.util.boundary, boundary.break
    
    def bar(a: Option[Int]): String = boundary:
      a.foreach:
        case 0 => break("zero") // throws boundary.break
        case aa if aa < 0 => throw new IllegalArgumentException("negative")
        case _ => ()
    
      "non-zero or empty"
    

    For regular return values such carrier exceptions are limited to a local scope. For irregular return values, the carrier exceptions are not limited to the local scope (unless we demand it), and that is the main difference here between regular and irregular return values here.

    From that perspective, cases when a value is returned without using an exception as a carrier, can be considered as just an optimization.

If you think about these two examples together, the boundaries between return values and exceptions begin to disappear, to some extent.

But the special return path is exactly what throwing exceptions is about.
A normal pattern match not catching the exception is important to keep exactly that behavior intact, otherwise you get this weird behaviour that Ichoran mentioned.

If you want the exception just as a return value, you can just return the exception.
If you have an existing function you don’t want to modify, you can just use a conversion function like this:

inline def returnCaughtException[A](inline a: A): A | Exception = try a catch case (ex: Exception) => ex

And then you can pattern match over the result as in the Java proposal, while still being clearer which exceptions are caught:

  returnCaughtException(foo(-5)) match
    case _: Exception => println("negative!")
    case s: String => println(s)

See scastie: Scastie - An interactive playground for Scala.

Overall, it is not clear to me what you are asking for specifically. I think Scala handles the syntactic side of this better than Java as already mentioned.
How people think about exceptions and how they use them is up to them, and is not really enforced by the language (except only being able to throw subclasses of throwable, which is a JVM restriction if I am not mistaken).

1 Like

I’m not sure that the mentioned behaviour is really that weird, because we already can get it in try-catch blocks, and it doesn’t cause any confustion:

try foo()
catch
  case ex: Exception1 =>
    // Imagine bar() can throw exception too
    bar()
  case ex: Exception2 =>
    // Nevertheless, we realize that we are still catching exceptions from foo() here
    // and not from bar(), don't we?
    handleFooException(ex)

I’d argue that the special try-catch syntax for exception handling was simply inherited from imperative languages like Java and C++. And imperative languages have it that way because they cannot help but follow CPU instructions – that is the way how imperative languages work.

On the other hand, Scala offers higher level abstractions, it diverged from the imperative approach in many ways. But when it comes to exception handling, it still follows the “mental model” inherited from the imperative world.

Now, it is quite curious to watch, that Java(!) begins re-considering the century-old error handling paradigm. That JEP is a very first step, but if accepted, it may have further consequences.

It is not exactly about syntax sugar, there can be an interesting shift in the perception of exceptions on the way.

Just do it in a library and prove its utility that way. Rust did that with try!(...) turning into ...? because it turned out to be spectacularly useful.

I already wrote the function for you, so you can just start using it and show off how it improves things, if it does.

Keep in mind that Scala can also implement ? as a library, with slightly more syntax than Rust (but not much).

My code all looks like

def foo() = Ask:
  val lines = f.gulp.?
  val x = parse(lines).?
  x.validate_?()

It’s really clean and clear, and I can’t imagine that checked exceptions would do anything but clutter and mess up this pattern.

But maybe in different contexts it’s great, and you have what you need to show it in a real-world codebase, so you don’t have to wait on anyone deciding that we need to maintain feature parity with Java for a feature that we’re uncertain about.

I wouldn’t say that I’m exactly asking but rather thinking that…
Ultimately, Scala could ditch the try-catch-finaly syntax completely.

  1. First of all, finally is nothing but the legacy of the old imperative approach. In Scala it can be implemented as an utility function. If fact, the most popular effect frameworks already do it: guarantee in CatsEffect and ensuring in ZIO. But it can be implemented in plain Scala too. It could be implemented even in Scala2, but in Scala3 the implementation can be even more efficient, e.g.:

    extension [A](inline expr: A)
      inline def guarantee(inline fin: Unit) =
        try
          val res = expr
          fin
          res
        catch
          case NonFatal(ex) =>
            fin
            throw ex
    
    // Then, somewhere in the code
    foo().guarantee:
        println("called regardless of any exception")
    
  2. Then, try-catch syntax can be abolished by incorporating catch into match cases.

  3. Next, Scala could probably lift the requirement that every exeption has to be a subclass of Throwable. Of course, on JVM some kind of Throwable has to be used under the hood as a carrier (and Scala already does it for non-local returns internally). But it doesn’t have to be required for exceptions in general.

    For example, there are many cases when we want to raise a structured error rather than something with getMessage. The requirement to extend some Throwable regardless is quite annoying:

    case class ParseError(line: Int, nodePath: List[NodeReg], errorType: ErrorType)
      extends Exception(...)
      // ... or `extends RuntimeException`, or `IllegalArgumentException` ???
      // I don't really need it, I don't care, but I have to comply...
    

    It is worth to mention that it could also simplify cross-building between Scala-JVM, Scala-JS and Scala-Native.

Perhaps, I failed to explain it clearly, but a substantial part of my point is that…

If we think about exceptions as values and not something extremely special, then it becomes clear, that checked exceptions are useless, because they simply diplicate regular return values, but in a weird way. So looks like we are on the same page regarding the checked exceptions, aren’t we?

Yes, and we already have robust and widespread alternatives in monadic error handling, and robust and not-very-widespread but available alternatives in direct-style error handling (both with result types).

So aside from a compatibility layer, we hardly need to handle exceptions at all, and there’s always the option of packing them immediately into a return type and then not worrying about it (e.g. Try(...)).

1 Like

I don’t think that’s correct. finally is a legacy of allowing incompleteness. That you can re-implement it as guarantee is fine, but the same concept implemented two different ways is still the same concept.

Why do we need the concept?

We need it for exceptions because of the expectation that letting the control flow fall through is so common you don’t even want to mention it. That’s the norm.

When falling through without any explicit handling is the norm, you want a way to say, “But wait! I need to do this before it falls through!” Hence finally or guarantee or whatever you call it.

This is one of the core differences in outlook between exceptions (which by design propagate up until you’re ready to handle them) and result types (which receive at least token attention at every level).

Hmm. I’m not talking about the concept. The concept is fine. What I’m talking about is that try-finally syntax, that doesn’t seem necessary in Scala.

It was inherited from Java, where it was important, because the language was so poor, that the concept couldn’t be implemented cleary in any other way. Perhaps it was also important in early Scala days, but not in Scala3. With Scala3 inlines, the concept doesn’t need to be a part of the core language.

I’m kind of saying exactly that, but maybe in weird wording. What I’m trying to say is that the concept of some values that we may want to let fall through is so common, that we probably don’t really need the dedicated try-catch syntax for that. Moreover, we don’t really have to make such values extending some special interface like Throwable.

Well, I’m not so sure. To me, Rust hits the sweet spot–both very clear that there’s a fall-through and very minimal so it hardly disrupts one’s attention on the core logic.

The problem with transparent fallthrough is that it’s transparent. Once you need to figure out what happened, it’s a huge headache, and it’s so easy to do nothing that it encourages pushing logic up too many levels, beyond where one knows how to appropriately handle it. The return types and ? (or .?) make an immense difference, both in reading, and in forcing you to ask, just for an instant, “Wait, should I handle this here?”

If Valhalla ever gets finished, we’ll (probably) be able to have a result type in Scala on the JVM with similarly tiny overhead to Rust, and we already could potentially make Scala-native use a similarly low-overhead method. Not sure about JS.

To me, exceptions are the sorta okayish but outdated way to handle errors, and something equivalent to Result with ?/anyhow is the superior way, with monadic handling of Either an acceptable alternative.

For indirect control flow of non-error conditions, monadic Either or direct-style jumps (with ?) are clearly superior to exceptions.

So I just don’t see the reason to futz with exception handling at all except as a compatibility layer, and then it’s nice to have the syntax adhere closely to the traditional way it’s conceived because you’re basically thinking old-style until you can get into a nicer regime.

I do have a variety of exception-handling helper methods in my code. But they are all of this sort: safe traps exceptions and puts them as the failure branch of a sum type. Ask is an unboxed desired value or a boxed error type, but it catches exceptions and boxes them. Fu runs a virtual-thread future, but it catches exceptions in a threadsafe way. Resource.nice gives a closeable resource in a loan pattern, but catches exceptions too. And so on.

The existing try/catch/finally is plenty good enough to handle that. finally is nice; makes code a bit simpler. Dispensable, I agree, but nice, when doing the compatibility layer.

But living in exception-land more deeply, even if it’s a more value-like view of them, isn’t very appealing. For all the people doing monadic error handling, I don’t think it’s desirable either. So to me this argues awfully strongly for exception-handling features being implemented in libraries only at this point.

If we retire try, then the compiler can avoid the import conflict between util.Try and Trees.Try.

That alone would be a huge productivity boost for the small compiler team.

Although I once admired the uniformity of syntax for match and catch, it has broken down with the introduction of one-line catch without one-line match.

Instead of

try throw null catch case _: NullPointerException => "null"

we could

throw null match catch _: NullPointerException => "null"

and of course agnostically for irrefutable patterns

"[email protected]" match case Email(user, _) => user

The other grievous omission is the lack of natch syntax, which serves nicely to replace finally, as it is the “natural” exit from a computation, without respect to normal or abnormal completion.

We’re not supposed to use exceptions for flow control, but having sampled the forbidden fruit of non-local return and break, we can hardly do without it. I agree in part with the proposal to make aborts syntactically less baroque, though not with the goal of obscuring their exceptional nature (because of other effects such as whether a thread is interrupted).

Exceptions are not like normal result values that can be ignored for now; normal values can be discarded, but exception handling is part of every design analysis, even if no exceptions are handled; that is, exceptions are never ignored.

I would be onboard with throw v syntax that wraps my value and unwraps it in catch v: V.

e match
case K => "k"
catch Erroneous(fail) => s"failed with $fail"
case if p => "predicate held"
natch => println("how'm I doing?")
match
case "k" => throw Error("expected to fail as usual")
catch Fail(pfail) => s"failed evaluating predicate with $pfail"
catch r: RuntimeException => s"little hiccup $r"
case x => s"some result $x"

I don’t see why the syntax should force me to write my catch later or my natch last of all.

e match
catch Error(msg) => handle(msg) // handle early exit
case ...

I tossed in case if but not case => to replace case _ => because I use underscore to mean “I’m not interested in the selector” and not merely “I don’t need to rebind it”. It is analogous to underscore in an import clause. If I use the value:

v match
case v => s"It was $v" // redundancy indicates I intend to use the value,
                       // and the name is nearby, if the selector is many lines away

For completeness, case x where the pattern is just a var could be called a patch for “pattern char”, though I don’t propose syntax for it.

Actually, the x match / catch syntax with no case is pretty nice, with try being inferred.

Thank-you, Rex, I knew I was going a bit over-the-top and I expected any response to be dismissive, so I appreciate your appreciation of my tweak to the OP syntax.

Let me add that I love Scala syntax try e catch f as an innovation of the Java syntax.

I am still learning what match means. It has something to do with values or a domain of values and how we might select among them. I like the current proposal that says, we also produce results by exception, and match can match them.