Pre-SIP: Checked Exceptions

Nevertheless I have written too many decorators around checked exception similar to

So I can say there are problems in practice.

I like the term error code\result code.

I agree with you.

I do not like an idea to process “error codes” in “try{}” because it is a point of confusion.
You can process a result in different try blocks and it is not obvious where it is right to do.
It is often not obvious what to do wrapping exception or throwing it again.

I think it would be better to write

  //if someone does not handle it he always wraps it. 
  val header = compileHeader(rawScript).getSuccess
  // there must be the only place to handler error codes
  val footnotes = compilerFootnotes(rawScript).catch {
        case  e:Error1 = >
   }

That’s why this proposal introduces a new syntax for wrapping / decorating exceptions (under section Exception wrapping):

// library code
def unsafeJdbcOperation(): Int ^ JdbcExceptionA | JdbcExceptionB | ...

// your code
def foo(): Bar ^ MyAppException = {
  ...
  val i = unsafeJdbcOperation() ^ e => MyAppException("additional context", cause = e)
  ...
}

That demonstration was about converting nested exception-throwing code to an either-oriented one. The resulting either isn’t necessary; I was comparing it with the pure-either code to produce a similar result.

I used Try {} .toEither because it’s more concise than:

try {
  ...
   Right(script)
} catch {
  case e => Left(e)
}

I’ve always felt that the main value of checked exceptions was not that they forced the caller to handle them but rather that they forced the thrower to declare them. Without that guarantee, it is easy to ascribe the wrong meaning to an exception, because it happens to leak through a method that you weren’t expecting it from.

Rather than forcing callers to catch checked exceptions, what if you automatically caught undeclared checked exceptions, wrapped them in an unchecked exception, and rethrew? I think that would address most of the objections to checked exceptions, but still preserve the requirement that they have to be declared to be thrown.

You might also issue a warning wherever the auto-wrapping code is needed, to give callers a nudge that they might want to handle the exception locally. An annotation could be added to suppress the warning locally, and a compiler switch could be added to suppress it globally. If desired, you could also add a switch to treat these warnings as errors, which would allow people who want it to opt into java-like behavior.

1 Like

I have meant that It is not a good idea at all using exception table entries to handle error codes.
It seems there are such disadvantages as:

  • overhead
  • puzzlement
    there are multiple level of possible handling
  • It is more difficult to integrate with scriptable languages
    I really do not want to wrap each such method in “try catch” in jexl for example

Here is some syntactic sugar

@main def main(): Unit = {
  
  def doSomething(): String^(E1|E2) = {
    Error(E2())
  }
  val result1 = doSomething().getSuccess()
  val result2 = doSomething().elseCatch{
    case _:E1 => 
      println("e1")
    case _:E2 => 
      println("e2")
  }
}
trait ^[S,E]{
  def elseCatch[B >: S](f: E=>B): B 
  def getSuccess(): S
}
class Error[S,E](e:E) extends ^[S,E]{
  override def elseCatch[B >: S](f: E=>B): B = {
    f(e)
  }
  override def getSuccess(): S = {
    throw new RuntimeException(e.toString)
  }
}
class Success[S,E](s:S) extends ^[S,E]{
  override def elseCatch[B >: S](f: E=>B): B = {
    s
  }
  override def getSuccess(): S = {
     s
  }  
}

class E1
class E2

As a point of curiosity, have you used Rust’s error handling features?

I find that they make error handling with sum types (Result in Rust, equivalent of Either) so easy that exceptions are almost always the wrong tool for the job.

  1. Exceptions are slow–you shouldn’t use them for error cases that can be handled and which arise more than negligibly often
  2. Exceptions capture stack but not data context throughout the stack, which is usually more important, rendering them unhelpful for debugging in many cases
  3. Exceptions favor long-range transport of error conditions rather than local handling, but in most cases the information needed to resolve or cope with an issue is local.
  4. Even with checked exceptions, the compiler can’t verify that all exceptional cases are handled (especially since some JVM exceptions are considered fatal and should not be caught!) since unchecked exceptions can arise nearly anywhere
  5. Exceptions don’t pass between threads, causing additional headaches any time error handling and multithreading intersect. (Which is approximately always.)

Because exceptions are usually the wrong tool, their footprint in the language should be small. Therefore, checked exceptions should only be considered if the impact on the language is essentially trivial. Unfortunately, this proposal has (1) a new keyword, (2) a new symbol, (3) apparently special rules for functions, (4) a new mechanism for binding a generic type identifier, (5) a suggestion of a new function definition operator, (6) new Enum-like syntax for custom exceptions, (7) a custom flatMap-like functionality, and (8) there are still admittedly four potentially problematic areas.

This is a huge footprint on the language which I don’t think can be justified given that there are much better ways to do most error handling.

In particular, the ? operator in Rust is brilliant (I have a macro that emulates it using return), and using map_err (or in Scala, .left.map) to add contextual information and/or change error types makes intentional propagation of relevant information easy.

Additionally, people who like monads already have toolchains that can deal effectively with error cases in a functional context.

So while I think it’s worthwhile considering whether checked exceptions can be added with barely any additional cognitive overhead on the part of the programmer, this approach of inventing a whole new error handling scheme built off of a usually undesirable mechanism is not advisable.

Possibly an effects system can capture this. Possibly something built off of opaque types can capture this, where opaque type Exceptional[E, A] = A simply marks that a checked exception can occur.

But this proposal is way, way, way too heavyweight, and I would actively avoid most code that used exceptions more heavily, even with these admittedly significant improvements to their usability, as there are better options available.

11 Likes

I like this point about reducing language footprint for exceptions. I’d assume that potentially it could be possible to get rid of try…catch syntax in Scala at all if favor of more generic match…case syntax.
I mean it could be possible to consider exceptions as just return values which are not specified explicitly. But this is just my thoughts anyway…

I wish there is a dislike button here. Scala is not Java.

3 Likes

@Ichoran First, let me thank you for taking this seriously and replying with a constructive criticism. It is clearly not trivial to do so given how controversial this topics is – as seen by other replies here – and I very much appreciate your way of conduct.

Although I disagree on some of the problems you listed, I do agree with the general notion that the proposal will introduce far too many new constructs and rules, serving only a particular use case.

I believe this use case is rather common, and that existing tools do not satisfy the need for a clearer code. I think I have another idea, though – less intrusive – which I will explain in a separate comment.

I did not, as I’m not familiar with Rust all that much. I did however examine it a bit now that you’ve mentioned it, and I agree that is quite a neat feature (so thanks for the reference!).

I still believe it is not sufficient – for two reasons – but then again I’m not entirely familiar with this feature, so correct me if I’m wrong.

The first reason being is that this feature is not entirely compatible with higher-order functions. For the sake of demonstration, let’s imagine that this operator exists in Scala:

def foo(): Result[Int, Error] = {
  unsafe()? + 2
  // is equivalent to
  val i = unsafe() match {
    case Ok(i) => i
    case Err(e) => return Err(e)
  }
  Ok(i + 2)
}
def unsafe(): Result[Int, Error] = ???

This surely reduces a lot of boilerplate, and it might b e worth having this feature – perhaps via macros? – regardless of error handling.

When we consider code with higher order functions, though, this feature cannot help reducing the boilerplate the same way exceptions do:

def foo(): Result[Int, Error] = {
  Seq(1,2,3).map(i => unsafe(i)?).sum
}
def unsafe(i: Int): Result[Int, Error] = ???

This example will not compile, as the anonymous function (unsafe?) returns Result of Ints, and sum is not defined on non-numeric items. It’s not necessarily a problem with the sum method in particular, but a general problem of seamlessly “reducing” a container of Results to a singular one.

It might be possible to overcome this problem by making the the operator behave differently in anonymous functions. Instead of making the function return type a Result, its return type should be the valid result, but it should still use return for the invalid result (err), and would still require the outer function to return a Result.

Say, if the compiler was smart enough to translate the above example to this:

def foo(): Result[Int, Error] = {
  val is: Seq[Int] = Seq(1,2,3).map { i =>
    unsafe(i) match {
      case Ok(i2) => i2
      case Err(e) => return Err(e)
    }
  }
  Ok(is.sum)
}
def unsafe(i: Int): Result[Int, Error] = ???

In which case the return actually throws a NonLocalReturnControl exception (if I’m not mistaken), and this functionality is said to be dropped in Dotty, so I’m not sure whether this is feasable. Also, I’m not entirely sure whether the relaxation of the "must return Result" restriction for anonymous functions would not incur other problems.

The second problem with this mechanism is that it doesn’t solve the boilerplate of multiple levels of nested exceptions:

def compile(rawScript: String): Result[Script, Err] = {
  val script: Result[Script, Err] = {
    if (rawScript.isEmpty) return Err("script is empty")
    val header = compilerHeader(rawScript)?
    ...
    compose(header, body, ...)
  }
  script.expect(s"Failed to compile script: '$rawScript'")
}

def compilerHeader(rawScript: String): Result[Header, Err] = {
  val rawHeader = rawScript.substring(0, 15)
  val header: Result[Header, Err] = {
    findBadWord(rawHeader).map { badWord =>
      return Err(s"bad word found: '$badWord'")
    }
    ... // other unsafe operations
  }
  header.expect(s"Failed to compile header: '$rawHeader'")
}

def findBadWord(s: String): Option[String] = ???

This code is a bit more cumbersome than the exception-based one; but more importantly, the explicit returns will ignore some of the expect messages so they won’t be included in the final result.

To clarify, I would expect an error message like so (could be reversed order):

bad word found: 'voldemort'
-- caused: Failed to compile header: 'voldemort rules'
-- caused: Failed to compile script: 'voldemort rules, not dumbledore'

Edit: To correct what I said earlier, nested returns are not dropped in Dotty, but merely replaced with a non-keyword mechanism that is slightly more verbose (the underlying implementation still uses exceptions).

Also, it’s probably worth noting that exceptions are only intended for error handling in sequential code. Concurrent code has other solutions (coroutines, messages, etc), albeit construction of multiple levels of nested errors is problematic (this is due the nature of concurrent code).

Inspired by Dotty’s replacement for non local returns, I created a Result that could potentially overcome the problem of multiple levels of errors:

def compile(rawScript: String): Result[Script] = {
  returning { implicit c =>
    if (rawScript.isEmpty) err("script is empty")
    val header = compileHeader(rawScript).?
    compose(header).?
  }.expect(s"Failed to compile script: '$rawScript'")
}

def compileHeader(rawScript: String): Result[Header] = {
  val rawHeader = rawScript.substring(0, 3)
  returning { implicit c =>
    findBadWord(rawHeader).map { badWord =>
      err(s"bad word found: '$badWord'")
    }
    new Header
  }.expect(s"Failed to compile header: '$rawHeader'")
}

def findBadWord(s: String): Option[String] = Some("bad").filter(s.startsWith)
def compose(header: Header): Result[Script] = Ok(new Script)

A full working example is available in this scastie (Scala 2).

I think this still need some revising. I don’t like needing to declare the implicit controller each time, nor do I like that err is just a method and not a keyword; after all, it stands for a control-flow mechanism and hides a throw call.

Coincidentally enough, this could look a lot like my “other idea” that I mentioned before. I had the intention of explaining that idea, but this experimentation with Result has led me to a similar solution.

Uh, no, if you expand x.? in Scala into x match { case e: Err(_) => e; case Ok(x) => x } then in fact unsafe(i).? is an Int. It works this way in Rust and in my macro in Scala.

So it works fine already. It never has a Result return type; ? is specifically designed to get out of the result type.

Your initial example should be unsafe().map(_ + 2), and then everything is fine.

Yeah, that’s how it works, and the feature is still there, just gated by wrapping with a returning {} block.

Also, you don’t need a separate Result type. Either works fine. (You can make ? work on Option too. I do.)

Isn’t the return type of such an expression is Int | Err?

That is, unless the err case returns the err: case e: Err(_) => return e;, which is what I showed in the next code example (“say, if the compiler was smart enough…”).

How so? The intent is to map a sequence of integers using an unsafe function, sum the mapped integers, and increment that by 2. I’d like to have an easy way to make this transformation:

val from: Seq[Result[Int, Err]] = Seq(1,2,3).map(unsafe)
val to: Result[Seq[Int], Err] = transform(from)

Which would be possible if the compiler will be “smart enough” to translate the ? in the anonymous function to code with return e. I mean, that is the default code it’s being translated to, I’m just not sure if this happens with anonymous functions as well.

Yeah, I corrected myself later. I’ve also used this pattern as an inspiration for something tailored specifically to Result.

Yeah, I don’t think I mind which one will be used eventually.

Yes, that’s exactly the point of the feature, in Rust as well as Scala.

In Rust, Result types are ubiquitous but there are only local returns.

Yes, that’s how it works. Right(Seq(1, 2, 3).map(unsafe.?).sum) is enough.

(Scala is not very good with postfix ops, so I use it as .?.)

(You can add a method to make things adopt the right branch, e.g. Seq(1, 2, 3).map(unsafe.?).sum.inRight.)

@Ichoran then it really look like a solid solution for these specific use cases of error handling; it seems that checked exceptions can safely retire (or die? they are already retired).

I’m also seeing that there is already a thread on rust-like results, so I’ll go read it.

Thank you for taking the time to help :slight_smile:

Sorry to jump in this discussion, (maybe the other link discussing Result is a better place to ask) but, even though I totally agree with @Ichoran about Result (or Try or Either) being a very good way to deal with errors, could we watch this thread from the Java interop point of view?

Would it be possible to imagine a compiler feature that turns Java’s checked exceptions into this Result we’re talking about ? (and what would happen if the Java code can throw multiple exceptions?).

I’d be really interested in such a feature, when we think about it, Java’s checked exceptions are a first step into “encoding the error into the type system”. Far from being perfect, but I think having an automatic feature to turn everything we can “understand” from Java’s libraries (non-nullable, nullable, side-effects, etc.) would make interop so great.

Please let me know if this is completely out-of-topic, or simply bad, dillusional or whatever :slight_smile:

Thank you.

EDIT: I don’t want my comment to sound as a “what if”. There’s certainly a good reason why checked exceptions cannot or should not be turned into such a Result type. Just a pointer to a good doc, a paper, or something else would be a nice answer, thank you.

@aesteve - Try already catches exceptions.

If you want to catch just the checked exceptions, you need type unions, because you can get any one of the checked exceptions. So Scala 3 could possibly support this.

I’m just not sure it’s worth it. It’s quite a bit of compiler tooling and a bit of complexity in the language just to support a feature that overall isn’t as good as something that we already have.

2 Likes

Glad you found it useful!

I should point out that although I wrote my own .? macro and use it ubiquitously, this style of programming is not common in Scala. However, as a proof of concept that we have better-than-checked-exception capability in Scala, it works nicely. (And though it’s not common, basically all of my code uses it because it’s so effective at dealing with errors.)

2 Likes

Yes for sure, I could not tell if it’s worth the effort or not :\

It sounds “magical” (in the good sense of the term) and very useful to me.


A little back-story, in case it may help:

We’ve been working in Scala on some module that relies on an old Java module of ours. The old module dealt with data Validation through JSR-303 and added checked exceptions on top of the ValidationExceptions.
Obviously an Either-like system (or Valid/Invalid like in Vavr) would have been better here. But I can’t blame people who wrote this module ages ago to have, at least!, encoded possible errors in the Exception system. That’s all they had at this time, and to be fair, that’s really better than RuntimeExceptions, or some weird binary-encoded return code.

When we dealt with this dependency in the new Scala module, we were a bit disappointed to have to wrap every call to multiple methods within a Try. In fact, we started to feel really reliant on the compiler that helped us so much and suddenly that “compiler-as-a-security-nest” felt apart.


I’m definitely not the right person to argue in favor of such a feature, maybe it’s not worth the effort, maybe it’s not desirable at all, but hopefully the example we faced will be helpful.

As you said, when we first had a look at union types we thought about checked exceptions and it “felt like” some glue could have helped us automatically.

Thanks for your answer :slight_smile:

I’ve already experimented with early returns from for-loops over Eithers:
https://users.scala-lang.org/t/dont-use-return-in-scala/3688/24?u=tarsa
https://users.scala-lang.org/t/dont-use-return-in-scala/3688/35?u=tarsa

That was for the unbiased Either in Scala 2.11-, but it should be easily convertible to biased Either in Scala 2.12+ (I can do that if you want).

The machinery also could probably be simplified, e.g. Either[Either[Error, Result], Result] could be changed to EarlyEither[Error, Result] that has methods like def flatMap[B](A => Either[E, B]) (to allow composition with ordinary Eithers) and def extract: Either[E, A] (to designate early return boundary).

Automatically translating a java method to an Either[Coproduct of declared throws, Result] sounds kind of neat, TBH.

3 Likes