Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit Exception rethrowing #3094

Closed
hazeycode opened this issue May 21, 2023 · 3 comments
Closed

Explicit Exception rethrowing #3094

hazeycode opened this issue May 21, 2023 · 3 comments
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists

Comments

@hazeycode
Copy link

hazeycode commented May 21, 2023

Currently is is possible to call functions without knowing if they throw. This leads to a sub-optimal developer experience and can easily result in a poor experience for the end application user.

Especially since there is no indication in function signatures that an exception may be thrown or not; I have to read/search the implementation of every function that I depend on, liberally wrap swathes of code in try blocks or take the path of least resistance and just not handle errors until they later surface as a problem in production.

I am proposing that exception handling be mandatory when calling functions that throw i.e. making the current implicit behavior explicit.

A basic example:

void foo() {
  if (somethingThatFails()) {
    throw 'something failed';
  }
}

foo(); // Compile error: "Exception not handled"

Remedy the compile error by explicitly catching and rethrowing

try {
  foo();
} catch (e) {
  rethrow e;
}

We could also consider a shorthand syntax, either by introducing a new keyword or maybe by overloading rethrow:

rethrow foo();

I believe this would be a better developer experience, improving readability, prompting users to think more carefully about what error conditions may arise in their programs and ultimately result in higher quality code being written in Dart.

I suspect that because errors/exceptions are not encoded by the type system, that this would unfortunately be non-trivial to implement. Apologies if I am making incorrect assumptions. I am a Dart noob and not a PL nor compiler expert.

@hazeycode hazeycode added the feature Proposed language feature that solves one or more problems label May 21, 2023
@eernstg
Copy link
Member

eernstg commented May 22, 2023

Please take a look at #984, which is a proposal to add support for checked exceptions to Dart. It was closed as 'state-rejected', which means that the discussion is unlikely to be reopened, but at least you can see what various people said about this topic.

@lrhn
Copy link
Member

lrhn commented May 22, 2023

All code can throw an error. Errors can be thrown for reasons not predicted by the code (StackOverflowError), or by using an API incorrectly (x ~/ y where y ends up being zero, or x << y where y is negative, neither of which can be seen from the type).
Having to do something special for a function which might throw means having to do it for every function, operator, getter and setter. That obviously doesn't scale. You'd have to write rethrow o.x = 1;, because o.x might be a setter which might throw.

You shouldn't be catching errors. They are mistakes, and it's OK if they crash the program, because there is no telling which state the program is in after getting rudely interrupted in the middle of something.

The throws that you should catch are exceptions, which usually implement Exception.
To treat functions which throw exceptions specially, you need to know which exceptions they throw, so that you can catch and handle them, or rethrow them (and document that you also throw the same exception).

That's basically Java's checked exceptions.

Dart doesn't do that, for a number of reasons.
One is the same reason why nobody has done that since Java, because it was very hard to work with in practice. Like C++ const, only ... worse.

A second reason is that it affects function subtyping. Dart has first-class functions as well as overriding (Java has overriding, so it's not a new issue).

If a method declares as int doStuff() throws FormatException {}, then subclasses cannot add more exceptions to that, because then it won't be "Exception sound" to upcast to the supertype and call without catching the other exceptions.
Generally, a subclass cannot add exceptions. That's annoying for general interfaces, because they need to predict all problems that subclasses can have.

For Dart, first class function types would also need to carry the exceptions.
You can't do:

int Function(String) parse = int.parse;

if int.parse throws FormatException. Even if you know you're only going to call it with arguments which won't throw.

(FormatException is somewhere between an error and an exception. It's error-like because it won't happen if you just make sure to only pass arguments which have the correct format. But checking whether something has the right format is almost as expensive as the parse function which throws FormatException, so it's meaningless to require you to pre-check the format.
If we were designing it today, I'd probably make it a FormatError and say that you should only use it with pre-checked arguments, and defer unknown arguments to a tryParse which returns null on bad inputs. Maybe. May we should have three functions, if distinguishing errors from exceptions become a language feature.)

All in all, the lines are not so clear-cut, and any required overhead is going to be annoying in the cases where it's not needed, and the caller knows that for purely semantic reasons that cannot be expressed in the type system.

I agree with @eernstg that this is unlikely to get a new response today. If anything, with nullable types and record return types and patterns, there are now more ways to report an exception than just throwing an Exception.
Having just throws with no types on functions which throw Exceptions which should be handled, could probably work. Any catch handler around them would count as handling, with no check that it actually catches all possible exceptions. It's only to ensure that you don't miss that an exception can be thrown.
But it's still very likely that we'll just see try { doFoo(); } on Object { /* won't happen */} code, and that's not helping anyone.

@lrhn lrhn closed this as completed May 22, 2023
@lrhn lrhn added the state-duplicate This issue or pull request already exists label May 22, 2023
@hazeycode
Copy link
Author

hazeycode commented May 22, 2023

Thank you both for your considerate responses. What's been said makes a lot of sense.

I actually think Exceptions should not be used for usual runtime error handling. I don't mean an error in the sense you have described above but rather modelling of bad conditions i.e. handling invalid data at runtime. I much prefer to use error union return types or nullables to communicate and handle these cases.

Unfortunately, the Dart docs explicitly recommend Exceptions be used for this purpose. In my own code I can simply not use exceptions and instead use error unions or nullables. Unfortunately code that I depend on does use Exceptions for this. And many more will take this path that is being recommended.

It seems to be that Dart Errors (as you describe above) are the only valid case for exceptions. And that Dart Exceptions that are not of this class have no practical use, perhaps harming the language. At the very least, other error handling methods such as nullables or error unions should be recommended by the docs and use of Exceptions discouraged.

Forgive me but the error handling story in Dart is very confusing and disappointing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

3 participants