BorrowScript
BorrowScript copied to clipboard
[Proposal] Exceptions
Exceptions are great ergonomically. They might be expensive as there is additional runtime code that needs to be shipped into the binary to support them.
In the first version, I will recommend using Go style tuples with a nullable error as the second type.
const [ value, error ] = stringToNumber("not a number")
if (error != null) {
console.log('Failure')
} else {
console.log(value)
}
Later, once we have a working compiler, if the overhead of adding exceptions isn't too great, we will have a discussion on their introduction.
I'd consider using union types for errors. Especially since Typescript has such nice support for them.
I use a lot of [err, val]
in my TS code when I need to explicitly check the error because I find try catch
makes code look messy.
But by default I think it makes it too easy to silence errors, and makes the code too verbose.
err
before val
is common because it means that it becomes explicit that an error is being silenced.
Rust's question mark is actually really nice, although there is not a nice TS counterpart for it.
Personally, I like exceptions only as a means of handling "exceptional" errors.
For example, in something like a request router or a middleware stack, being able to put an exception handler at the top makes a lot of sense, if you don't want the entire service to shut down because of an error in one controller or middleware component.
On the other hand, when developers throw exceptions for completely normal and expected conditions, is where exception handling as such begins to fall apart. For example, there is nothing exceptional about trying to open a file that doesn't exist, or attempting to open a port that isn't available - these are completely normal and expected conditions, and handling them really shouldn't be optional.
I think I'd like to see a language with two mechanisms for this: one for "exceptional conditions you should handle", and another for "errors you probably only want to handle in an error-handler".
Interestingly, the exception class in JS is actually called Error
- so it might be reasonable to describe Error
and try-catch
in BS as the mechanism for "errors you probably only want to handle in an error-handler".
One answer for "exceptional conditions you should handle" of course is [err, val]
tuples, as suggested by @vjpr, although personally I'm not keen on tuples, or the concept of multiple return-values. Conventions are flexible - you don't have to follow them exactly; that's sometimes and advantage and sometimes a problem. But I think a feature like this is important enough to warrant a language feature, rather than just a convention?
If we're going to have throw
and try-catch
, maybe it would make sense to build on that mechanism, to improve it's ergonomics and make it more useful for "exceptional conditions you should handle".
A few options come to mind:
🤔 Separate base types for Exception
and Error
, and the ability to catch (Error e)
etc. - like e.g. C# or PHP. This approach, arguably, may not be very explicit or transparent - if people derive their own types from them, it doesn't make the code directly readable without drilling through those types to learn how they're implemented. It's a run-time mechanic for dynamic languages, I guess - might be better to have a compile-time mechanic in a language like this one.
🤔 Something like Java's throws
keyword in function declarations, but optional: if a function declares it's going to throw something, this would force the immediate caller to handle it. This approach is more explicit and probably a better candidate for a language favoring compile-time mechanics. But there's also a hint of "restating your assumptions" by having to both throw and declare that you're going to throw. It requires two validations: did the function throw what it said it would throw, and did the caller catch?
😀💡 Some means of explicitly throwing exceptions "directly at" the caller. I actually don't know if any language has this? But some simple annotation on the throw
statement, like throw!
or throw hard
(haha) which actually taints the enclosing function signature, forcing the caller to immediately handle it.
The latter could optionally come with some kind of shorthand alternative to the wordy try-catch
- an expression rather than a statement, so perhaps something along the lines of Rust's ?
. I don't know precisely how that one works. I like the or
keyword in V, which is somewhere between an expression and a statement, providing a block for the actual error-handling. (Note that this is limited to handling one type of error - I'm not sure if that's a deliberate/meaningful limitation or not?)
Have you considered using something similar to Rust's Option/Result? Sum types force a user to handle the error, and in my opinion, are more ergonomic than Java-style checked exceptions as they are "built-in" to the return type. They're similar to using null, and could be implemented in a more simple manner than Rust enums, but are safe.
I like having an official Result
object used for error handling to take the place of the suggested tuple concept and enforce consistency.
But it's also a little ugly. One thing I find cumbersome in Rust is constantly unwrapping results. The ergonomics are also difficult when you think about how Result
interacts with Promise
I like how Option
and Result
becomes part of function signatures. You have to handle it "up front". And a whole bunch of programming errors is no more, like forgetting to handle an exception etc. It's been written much about this, so just my 2c. But I agree that the ergonomics could be better than Rust, or perhaps use a bit of sugar.