|
The way I see it, the issues with Exceptions are with 1) types and 2) the try/catch syntax, and the issue with Error-As-A-Value is that it's cumbersome. Exceptions solve a very real problem. Sometimes I get to the point where there's nothing more I can do and it's time to start unwinding the stack. I eventually either signal with try/catch that I'm ready to start handling the issue somewhere up the stack or never do and I crash. Error-As-A-Value addresses the "types" problem (specifically Option types do this; Go ignores this problem AFAIK and errors are poorly supported by the type system) and "forces" users to be explicit, except they can always just ignore the value when they need to anyway but now with added boilerplate. Just as importantly, they propagate this boilerplate to any caller, even if the caller doesn't care. Having to say within each and every caller, no, I really don't care about this error and there's nothing I can do about it right now is tedious, cumbersome, and often truly introduces no value. I think we can do better than either by allowing the use of both. What if I had the compiler and other tooling keep track of the Exceptions that can be thrown? const RandomError = new Error("you have bad luck!")
const DivideByZero = new Error("cannot divide by zero!")
// this can only throw RandomError
const maybeAdd = (a: number, b: number): number => {
if (randrange(0, 1) > 0.5) throw RandomError
return a + b
} ?? RandomError
// myFun can throw RandomError or DivideByZero, and our tooling
// will help us keep track of that.
const myFun = (a: number, b: number): number => {
if (b === 0) {
throw DivideByZero
}
return maybeAdd(a, b) / b
} ?? DivideByZero
Well, in TS/JS, now I still need to use try/catch at some point to handle the exceptions this will eventually throw. But maybe an error-as-a-value makes more sense. What if I included sugar that optionally replaced try/catch with error-as-a-value, if that's what the use case called for? type Result = {
ok: number
error: DivideByZero | RandomError
}
// myFun(1, 2)? will return the result type indicated above
let { ok, error } = myFun(1, 2)?
while (!ok) {
{ ok, error } = myFun(1, 2)?
}
return ok
This is an unfortunately contrived example but I think it demonstrates my point. I don't really see any reason we can't have both in modern languages.1. The problem with "types" in Exceptions being that you usually don't have any insight into whether or what errors can be thrown in a language that uses Exceptions as the main error handling control flow 2. The problem with try/catch syntax is subjective, but sometimes you don't want to introduce new scopes and at least 4 new lines. And code with extensive error handling becomes unnecessarily littered with try/catch when you would have preferred an abbreviated assignment expression as with error-as-a-value. |