BorrowScript icon indicating copy to clipboard operation
BorrowScript copied to clipboard

[Proposal] Exceptions

Open alshdavid opened this issue 2 years ago • 6 comments

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.

alshdavid avatar Sep 30 '21 08:09 alshdavid

I'd consider using union types for errors. Especially since Typescript has such nice support for them.

kazimuth avatar Oct 05 '21 01:10 kazimuth

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.

vjpr avatar Oct 05 '21 12:10 vjpr

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?)

mindplay-dk avatar Oct 09 '21 08:10 mindplay-dk

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.

chop0 avatar Nov 01 '21 08:11 chop0

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

alshdavid avatar Nov 11 '21 03:11 alshdavid

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.

NOP0 avatar Jan 04 '22 18:01 NOP0