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

Add checked exceptions. #984

Closed
bsutton opened this issue May 22, 2020 · 59 comments
Closed

Add checked exceptions. #984

bsutton opened this issue May 22, 2020 · 59 comments
Labels
request Requests to resolve a particular developer problem state-rejected This will not be worked on

Comments

@bsutton
Copy link

bsutton commented May 22, 2020

It has to be said...

The one thing I miss moving from Java to Dart is the lack of checked exceptions.

It makes it really hard to put complete error handling mechanisms in place if you don't know the full set of exceptions that a method can throw.

I find it rather ironic that dart doesn't have checked exceptions whilst 'Effective Dart' lints generate an error essentially saying 'check your exceptions'.

try {
      Directory(path).createSync(recursive: recursive);
    } 
    catch (e) {
      throw CreateDirException(
          'Unable to create the directory ${absolute(path)}. Error: ${e}');
    }

generates the following lint:
Avoid catches without on clauses.

To fix this lint I need to read source code to discover the exceptions that are going to be thrown.

Exceptions were created to stop people ignoring error.
Unchecked exceptions encourages people to ignore errors.

Have we learnt nothing?

I know this probably won't get anywhere due to the current religious movement against checked exceptions and the disruption to the dart eco system but this implementation decision was simply a bad idea.

@julemand101
Copy link

I do agree but you should properly move this issue to the language project: https://github.com/dart-lang/language/issues since the language itself does not support checked exceptions.

@lrhn lrhn transferred this issue from dart-lang/sdk May 26, 2020
@lrhn lrhn added the request Requests to resolve a particular developer problem label May 26, 2020
@lrhn
Copy link
Member

lrhn commented May 26, 2020

Most languages created after Java has learned from Java and not introduced checked exceptions.
They sound great in theory, but in practice they are (rightfully or not) vilified by the people having to maintain the throws clauses. I think the general opinion is that it's technically a good feature, but it doesn't carry its own weight.

I don't see Dart going that way at the current time.

To fix this lint I need to read source code to discover the exceptions that are going to be thrown.

You can write on Object catch (e) if you just want to satisfy the lint. Or you can drop the lint.
If you actually want to catch the exception being thrown (and you should when it's an exception, not an Error), then the method documentation should document it clearly.

The standard way to document exceptions is a paragraph starting with "Throws ...". I admit that not all code follows that standard, and dart:io suffers from being written before most standards were formed.

@bsutton
Copy link
Author

bsutton commented May 26, 2020

I admit that not all code follows that standard

And this is exactly the problem.

I would argue that checked exceptions more than carry their weight.
With modern IDE's the overhead of managing checked exceptions is largely automated.

The lesson that doesn't seem to have been learned is that developers are lazy and inconsistent (myself included) and if we don't force them to directly address errors then they simply ignore them and the quality of software suffers as a result.

I use a fair amount of flutter plugins and largely there is simply no documentation on what errors can be generated.
Dart actually makes it harder to document errors as a developer now actually has to write documentation.
Checked errors force devs to document their errors and actually make it easier as the IDE inserts them.

The end result is that for a large chunk of the code base we use on a daily basis, errors are simply not documented and we have to do additional testing and investigation to determine what errors need to be dealt with.

In the business world they have the concept of 'opportunity cost' which is essentially if I invest in 'A', I can't invest in 'B'. What is the cost not doing 'B'? That is the opportunity cost.

With unchecked exceptions we wear the cost of maintaining the checked exceptions but we don't have to wear the cost of investigating what errors can be generated nor the debugging time spent because we didn't handle an exception in the first place.

If a library maintainer has to spend time to declare the checked exceptions that time is more than offset by developers that use that library not having to investigate/test for what errors will be generated.

I believe the backlash against checked exception is because most developers prefer to ignore errors and checked exceptions require them to manage them.

@munificent
Copy link
Member

I think if a function's failure values are so important to be handled that you want static checking for them, then they should part of the function's return type and not an exception. If you use sum types or some other mechanism to plumb both success and failure values through the normal return mechanism of the function, then you get all of the nice static checking you want from checked exceptions.

More pragmatically, it's not clear to me how checked exceptions interact with higher-order functions. If I pass a callback to List.map() that can throw an exception, how does that fact get propagated through the type of map() to the surrounding caller?

You describe the movement away from checked exceptions as "religious", but I think that's an easy adjective to grab when you disagree with the majority. Is there a "religious movement" towards hand washing right now, or is it actually that the majority is right and hand washing is objectively a good idea? If almost everyone doesn't like checked exceptions, that seems like good data that it's probably not a good feature.

most developers prefer to ignore errors and checked exceptions require them to manage them.

You're probably right. But if you assume most developers are reasonable people, then that implies that it should be relatively easy to ignore errors. If it harmed them to do it, they wouldn't do it. Obviously, individuals make dumb short decisions all the time, but at scale I think you have to assume the software industry is smart enough to not adopt practices that cause themselves massive suffering.

@bsutton
Copy link
Author

bsutton commented May 29, 2020 via email

@bsutton
Copy link
Author

bsutton commented May 29, 2020

Just a final thought.

Why would you move from a system (checked exceptions) which automates the documentation of code to one that requires developers to document their code.

The empirical evidence (again look at pub.dev) is that developers do a terrible job of documenting and view documentation as a burden.
Managing checked exceptions is a far smaller burden than documenting code.

The lack of checked exception really has to be the best example of developers shooting themselves in the foot :)

@leafpetersen
Copy link
Member

Why would you move from a system (checked exceptions) which automates the documentation of code to one that requires developers to document their code.

My general take on this is that effect type systems (of which checked exceptions is one) tend to interact badly with higher-order code, and modern languages including Dart have leaned in heavily to using first class functions in libraries etc. Indeed, the first search result I got when I just looked to see what Java does with this these days was this, which isn't inspiring. There may be better technology for making this non-clunky now, but last I looked at it, it took some pretty hairy technology to be able to express higher-order functions that were parametric in the set of exceptions that could be thrown. Inference can help with that, but as the saying goes, now you have two problems... :)

The empirical evidence (again look at pub.dev) is that developers do a terrible job of documenting and view documentation as a burden.

I will admit that it personally bothers me a lot that even for well-documented Dart team owned code, I often have to go poke around in the implementation to figure out whether an exception can be thrown, and in what circumstances.

@rrousselGit
Copy link

rrousselGit commented May 29, 2020

Exceptions were introduced to solve the failings of return types.

  1. having to pass return types up the call stack.
  2. using the return to indicate error conditions means that you can't use
    the return type to pass the 'good path' values.
  3. poor documentation
  4. devs choosing to ignore error codes.

Exceptions are not the only way to solve these.
There are multiple languages out there with no exception mechanism at all and that do just fine.

The alternative is usually a combination of union-types and tuples and destructuring. This leads to self-documenting code, where it's impossible to ignore errors, while still being able to return valid values

For example using unions, instead of:

int divide(int value, int by) {
  if (by == 0) {
    throw IntegerDivisionByZeroException();
  }
  return value / by;
}

void main() {
  try {
    print(divide(42, 2));
  } on IntegerDivisionByZeroException catch (err) {
    print('oops $err');
  }
}

we'd have:

IntegerDivisionByZeroException | int divide(int value, int by) {
  if (by == 0) {
    return IntegerDivisionByZeroException();
  }
  return value / by;
}

void main() {
  switch (divide(42, 2)) {
    case IntegerDivisionByZeroException (error) => print('oops $error'),
    case int (value) => print(value);
  }
}

@bsutton
Copy link
Author

bsutton commented May 29, 2020

I'm not certain unions result in more readable code than catch blocks.
If I'm reading the code correctly it does provide a level of documentation but it appears that it will still allow the caller to ignore the error and fail to pass it back up.
So once again we are dependant on the developer to do the correct thing and we are stuck with undocumented code.

Tuples will have the same issues.

@bsutton
Copy link
Author

bsutton commented May 29, 2020

My general take on this is that effect type systems (of which checked exceptions is one) tend to interact badly with higher-order code, and modern languages including Dart have leaned in heavily to using first class functions in libraries etc

This appears to be a broader problem with how do you handle errors in lambda etc.
Whether its a checked/unchecked exception, error, union or tuple you still need to handle the errors in the top level function.
Error returns simply allow you to (incorrectly) ignore the error.
Your code looks nice, but does it actually behaviour correctly?

@rrousselGit
Copy link

Error returns simply allow you to (incorrectly) ignore the error.

That is not the case.
Unions forces you to check all possible cases, or it is otherwise a compilation error.

Continuing with the code I gave previously, it would be impossible to write:

int value = divide(42, 2); // IntegerDivisionByZeroException is not assignable to int

You would have to either cast the value or check the IntegerDivisionByZeroException case.

This is in a way non-nullable types, but broadened to apply to more use cases

@bsutton
Copy link
Author

bsutton commented May 29, 2020

OK, sure.

There however seems little point to introducing a new mechanism when we already have exceptions in the language.
We could possible even start by requiring checked exceptions via a lint.

We need some language experts to comment on this.

@leafpetersen
Copy link
Member

We need some language experts to comment on this.

Touché. I'm afraid we're the best you're going to get though - Google can't afford to hire better.

@bsutton
Copy link
Author

bsutton commented May 30, 2020 via email

@bsutton
Copy link
Author

bsutton commented Jun 1, 2020

So I come back to my evidenced based approach for this feature:

I would advocate that Google do an analysis on the level of documentation
for errors in pub.dev.

If it turns out that errors are well documented then I will withdraw my feature request.

Let's now find out if decisions on checked exceptions are driven by religion or science.

@munificent
Copy link
Member

So I come back to my evidenced based approach for this feature:

I think the clearest, easiest-to-acquire evidence is that almost no users are requesting this feature despite ample evidence of its existence thanks to Java. Given that, I think the burden of proof is on those requesting it to show that it's a good idea. If the language team had to actively disprove every feature request, we'd never make any progress. :)

@bsutton
Copy link
Author

bsutton commented Jun 9, 2020

So what would be an acceptable metric?

@munificent
Copy link
Member

Pragmatically, I can't see what metric would be persuasive here. Even if we were all 100% sold on checked exceptions, adding them would be a massively breaking change to the language. We have too many users and too many lines of code in the wild. In order to justify a migration of that scale, we'd want to see hordes of users beating down the door demanding it (which is what we do see, for example, for non-nullable types). I just don't see the user demand for this, at all. And, ultimately, we are obliged to make a thing that users want.

@bsutton
Copy link
Author

bsutton commented Jun 9, 2020

@munificent yes I do see that as a problem.

The 'somewhat vague' thought was this could be introduce via a lint so like types it is optional.

Sometimes you need to give people what they need rather than what the want :)

@leafpetersen
Copy link
Member

Pragmatically, I can't see what metric would be persuasive here. Even if we were all 100% sold on checked exceptions, adding them would be a massively breaking change to the language

In addition, I'd also add that it's not enough to have a metric that says there is a problem. You also need to be able to argue that a proposed solution is a good one. My objection (and I think most of the objections above) is not to the characterization of the problem, but to the solution. I think that checked exceptions are not a good solution to the problem, for reasons that I touched on above, and that is one of the reasons they have not become more widespread outside of Java. These kind of effect systems don't easily compose with existing language features (like generics and higher order functions). While encoding errors into return types can be a bit heavy, it has the great advantage that it is an approach which composes well with already existing (and otherwise useful) language features.

@bsutton
Copy link
Author

bsutton commented Jun 9, 2020

Encoding errors into return types has already been proven not to work which is why C++ had exceptions to fix the problem of C.

Encoding errors into return types simply results in users ignoring the errors which is the problem that dart has now due to the lack of checked exceptions.

I would argue that check exceptions is the best solution we have to the problem. Its just that developers are inherently lazy and don't like being forced to deal with errors.

The pub.dev code base demonstrates this in spades.

Whilst we do need to listen to the user base, we also need to ensure the health of the eco system and sometimes that means administering some medicine, even if it tastes awful.

@leafpetersen
Copy link
Member

Encoding errors into return types has already been proven not to work which is why C++ had exceptions to fix the problem of C.

There are many more modern languages which use this approach, either in it's monadic formulation, or directly, than which use checked exceptions, so I'm going to have to disagree with your characterization of the relative success of these approaches.

That said, I don't claim it's a wonderful solution. My take on the space is as follows:

  • There is a problem (though not clear how large of a problem)
  • There is a bad, and very expensive solution (Checked Exceptions)
  • There is a not great, but almost entirely free solution (encoding your errors in abstract return values)

I understand that the "not great" solution doesn't help enforce hygiene for the ecosystem, which is why I say it's not great.

I would argue that check exceptions is the best solution we have to the problem.

That may be, but it is still a bad solution. As far as I can tell, the best advice out there for working with higher order functions in Java is to take every single lambda, wrap its body in a try/catch which catches the checked exceptions that may be thrown in it, and rethrow an unchecked exception. Am I wrong? That helps nobody, and makes for a completely unusable language.

@bsutton
Copy link
Author

bsutton commented Jun 9, 2020

As far as I can tell, the best advice out there for working with higher order functions in Java is to take every single lambda, wrap its body in a try/catch which catches the checked exceptions that may be thrown in it, and rethrow an unchecked exception. Am I wrong? That helps nobody, and makes for a completely unusable language.

This is the crux of the problem.

You are essentially arguing that error handling makes the code look ugly so you would prefer to ignore the errors.

Whether the errors are a checked exceptions or a return type, if an error is going to be returned the lambda MUST handle the error.

Ignoring errors to make the code look nice is not an acceptable argument.

I too would prefer a more elegant solution to handling errors in lambda's but you are blaming the messenger (checked exceptions) for the problem.

The issues is really with lambdas not with checked exceptions.

Perhaps we need to look at how futures handle exceptions and whether some similar concepts could be applied to lambdas.

@leafpetersen
Copy link
Member

You are essentially arguing that error handling makes the code look ugly so you would prefer to ignore the errors.

Whether the errors are a checked exceptions or a return type, if an error is going to be returned the lambda MUST handle the error.

No! You are deeply misunderstanding the problem, and this gets at the crux of it! :)

The lambda is not the correct place to handle the problem. The correct place to handle the problem is higher up the stack. This can be clearly seen in the advice I quote above (which really is all over the internet - look for it)! The advice is not catch the error and deal with it the error is catch the error and throw an unchecked exception because the lambda is the wrong place to deal with the error. So the result is you get neither safety (your lambdas just catch and discard errors) nor brevity (you must explicitly catch and discard errors).

To make this actually work properly, you implement a full effect system, which lets your higher-order functions be parametric over the implied effects of their arguments. So your .map method is parametric over the set of exceptions possibly thrown by its argument. This works out ok, sort of, but it's terrible to work with, and an extremely heavyweight feature.

So you instead you get the mess that is Java checked exceptions, which, as I say, doesn't compose with higher order functions.

If you encode errors in the return type (e.g. using monads), then it's entirely up to the programmer where to handle the error. The lambda simply gets type int -> OrError(int) instead of int -> int, mapping gives you an Iterable<OrError(int)> and now to get at the contents, you need to explicitly iterate through either discarding the errors (nothing stops you from hitting yourself if you really want to), accumulating them (e.g. with a monadic fold), or handling them. But the point is that the programmer can choose at what level of the stack to handle the error.

I'm not saying it's ideal (it's not!) but it is strictly better than checked exceptions as implemented in Java, and it is entirely expressible in Dart as it exists today.

Perhaps we need to look at how futures handle exceptions and whether some similar concepts could be applied to lambdas.

Yes! We do need to! And ... it's... a ... wait ... for ... it.... monad! Futures are a monad, and they reify exceptions into the monadic value, so that the exception can be handled (or not handled) by the end user by binding the error and handling, rather than throwing an asynchronous exception from somewhere deep in the bowels of the runtime system (usually).

@simophin
Copy link

simophin commented Jun 18, 2020

Checked exception:

  1. Is infectious, from bottom to top;
  2. Because its infectiousness, developers will be encouraged to either:
    a. Swallow the error.
    b. Convert it to RuntimeException.

Both of which defeat the purpose of a checked
exception. These actions taken by developers are not rare, you see it everywhere! What ironic it is: that a feature designed to help write better code, will encourage you to ignore it?

If you talk about documentation purpose, surely a union type should be sufficient?

@OlegAlexander
Copy link

OlegAlexander commented Oct 11, 2020

An excellent discussion so far. I agree with @leafpetersen that adding checked exceptions to the language may not be the best solution to this problem because as @simophin pointed out, checked exceptions will not magically turn bad programmers into good ones. However, there is a problem here, especially when it comes to the documentation of exceptions. As you will see, the problem could be at least partially solved with some changes to dartdoc.

I've recently run into this issue with the glob package. Before I go on, I should say that glob is an excellent package! I'm only using it as an example to discuss potential improvements to the documentation of exceptions in Dart.

This line will throw a StringScannerException defined in the string_scanner package, which is a dependency of glob.

final invalidPattern = '';
final glob = Glob(invalidPattern); // Throws StringScannerException 

The 1.2.0 Glob constructor documentation doesn't mention the StringScannerException. Okay. But what if I wanted to document that my function, which uses Glob may throw a StringScannerException? Simply saying /// Throws [StringScannerException] at the top of my function doesn't work, because it seems dartdoc can only link to identifiers that are visible in the current file. So I have to first add string_scanner as a dependency in my pubspec.yaml and then import 'package:string_scanner/string_scanner.dart'; into the current file. Now dartdoc will recognize the link to [StringScannerException].

Having to directly depend on a transitive dependency for documentation purposes is not ideal. Also, a package you depend on may add/remove exceptions without warning, which may make your own documentation (and code!) out of date.

It would be better if dartdoc could somehow automatically enumerate all the possible exceptions a function may throw (except for the ubiquitous ones, like OutOfMemoryError.) In other words, the exceptions should be a part of the function signature, but this doesn't necessarily need to happen in the language. It just needs to happen...somehow 🙂

Thoughts?

@bsutton
Copy link
Author

bsutton commented Oct 11, 2020

I've raised an issue suggesting that we add a lint rule as a middle ground.

The lint rule would give most of the benefits in that exceptions would be documented without requiring a fairly significant changed to the language.

dart-lang/linter#2246

@lrhn
Copy link
Member

lrhn commented Oct 12, 2020

For what it's worth, that's actually design smell from the Glob package that its public API exposes a class from a different package, one which it doesn't export itself. I'd prefer if it caught the string scanner's exception and threw a FormatException instead.
If the analyzer had warned about the undocumented exception, then it's probably more likely that the package would have noticed and handled it.
(Edit: And they apparently do throw FormatException since StringScannerException implements it. It would still be better if that was documented in the public API.)

@DevNico
Copy link

DevNico commented Apr 30, 2021

I too don't understand the hate checked exceptions get. A simple use case your points (@munificent) don't cover is what if I have multiple calls to a function that may fail. e.g.

final value1 = await myObject.getSomeValue('value1');
final value2 = await myObject.getSomeValue('value2');
final value3 = await myObject.getSomeValue('value3');
final value4 = await myObject.getSomeValue('value4');

If I value1..4 were some kind of Result object I would have to do something like

if(value1.success && value2.success && value3.success && value4.success) { ... }

The same kind of long and repetitive if has to be written if the function returns null in case of an error.

If the function threw a checked exception I would be informed that I need to handle the exceptions and one try {} catch {} would solve the problem. This syntax would be way more readable and easier to maintain in my opinion.

Maybe instead of adding checked exceptions as a language feature, it could be added as an annotation? If I could declare my function with an @Throws(SomeException.class) annotation and then get analyzer support that way would already be a huge help since one could just configure the rule to act as an error.

Would be great if this could get looked into again!

@lukepighetti
Copy link

lukepighetti commented Apr 30, 2021

@mateusfccp I noticed you thumbs downed my comment. Care to weigh in on what I might have missed? What I proposed appears to be only benefit compared to what we have today. Nothing extra to do. But I may have missed something.

@munificent
Copy link
Member

I'm having a hard time understanding why checked exceptions are anything but value add.

I look at checked exceptions as essentially an oxymoron. Taking a step back, why have exceptions at all as a language? What is it that they do? Where is the value add?

The really useful thing about exceptions—their actual mechanism—is that they automatically unwind the stack and jump from the point of throw all the way to the point of catch. The obvious nice value is that you don't have to remember to unwind at each point where you don't want to handle the error. You can't silently drop the error on the floor like you can in C.

But it's more than that. It's not just that functions between the throw and catch don't have to manually rethrow the error. It's that they don't have to know anything about them at all. This abstraction and decoupling is really powerful. It means that exceptions can propagate through higher-order functions. It means that an overridden method can throw an exception type that the base class doesn't know about. It means that you can store a closure in a field and call it later from some other function without that second function being coupled to every possible exception that could come out of that closure.

OK, so then what do checked exceptions do? They force any code that could receive an exception to explicitly handle it or rethrow it. Any call frames between where the exception is thrown and where it is caught must explicitly choose to propagate that exception and are now directly coupled to the types of those exceptions.

Doesn't that just cancel out all the value of exceptions in the first place? You've made them no longer exceptions.

Let's say the goal of your API is this:

  1. It produces some kind of value on success.
  2. It produces one of a couple of other different kinds on failure.
  3. You want to ensure that callers handle all possible produced types.
  4. You don't want errors to silently unwind the stack.

What you want here isn't exceptions. You want a return value.

Now, there is an argument that this could be handled with union types and a robust where clause.

Yes, this, exactly. A "failure that you don't want to automatically unwind the call stack" shouldn't be modeled using exceptions because the entire point of exceptions is "automatically unwind the call stack".

@lukepighetti
Copy link

lukepighetti commented May 6, 2021

That makes sense, however I don't think it takes into consideration that my proposal above allows the consumer to not be forced to handle any exception at all.

No try/catch block? No problem.

No try/catch block and adding a new exception type? That gets added to the signature.

Try catch block? All exceptions are caught and are not added to the signature unless rethrown. That's consistent with our experience today, unless I'm missing something.

You gain the ability to know what errors are coming through the catch. And if you decide to use try/on it can either recommend that you add a catch block to rethrow, provide autocomplete to add an on block for each exception type, or just automatically rethrow anything not explicitly handled.

But if you have a function signature that cannot throw FooException and you wrap it with try / on FooException, it should throw a compiler error that says that's not going to happen.

If my proposal isn't clear I will happily create a more complete document.

@lrhn
Copy link
Member

lrhn commented May 7, 2021

The thing with checked exceptions is that it's an attempt to declare the computation instead of just its result.
(We can already declare non-termination using Never, so that's not entirely new).

We'll at least need the ability to abstract over computation. If we allow <int: FooException, BarException> to denote the results of a computation, then I'll want to be able to write something like:

<R:BarException, ...E> handleFoo<R, E*>(<R:FooException,E> Function() f) { // E* is a set of types
  try {
    return f();
  } on FooException catch (e) {
    throw BarException(e);
  }
}

That will make computations first-class abstractable operations, just like we needed generic functions to abstract over the types.

Then all functions taking a callback that they call directly will need to abstract over the exceptions, say:

 <List<T>:...E> List.generate<E*>(int n, <T:...E> compute(int n)) => ...

A callback stored for later usually won't be allowed to throw exceptions, unless there is an outlet for the exceptions.

Which brings us to async code, which is also a problem. We'll want Future to capture exceptions too, to be Future<R, E*> so that await f has type <R: ...E>. We'll want Stream<T, E*> to emit only E exceptions from the await for loop.

If you have to maintain the list of exceptions a method might throw or propagate, it's probably not going to be workable in practice. Java tried, and largely failed, that model. (It's amazing the amount of work people will do to work around the Java checked exception system).
So I want a syntax to write <int:> foo() { ... } to infer the exceptions thrown by the body of foo automatically (it's different from just int which means no exceptions). I'd expect most functions to use that syntax, but it's unlikely that we can do that with top-level inference, so it might not work.

Basically, we treat <T, E1... En> as different "return channels", which you trigger by either returning a value or throwing one of E1..En, and the result goes to the surrounding handler for those (which must exist), with the "return" handler being automatically at the call point. In that sense, a return is a throw which is automatically caught at the call point.) We just have t be able to treat all of them abstractly, not just the return value.
(I'm toying with a language design where all "return types" are basically union types, and if your function doesn't handle one of the return values of a function it calls, that function just returns out past your function to whoever is handling it. Basically, all returns are throws to the appropriate named handler, the trick is to ensure that there always is a handler. Which brings us back to @munificent's point: Checked exceptions are returns, just ones which are easy to propagate.)

@lukepighetti
Copy link

lukepighetti commented May 7, 2021

From a purely 'gut check' point of view, it would make sense to me that Errors act the way they do today, but Exceptions be checked. I admit that this could be handled with union types, but I fear that we have so much code where Exceptions are being thrown instead of returned that we'd (in practice) never get to the point where anyone was actually using this pattern. And on top of that, I personally don't feel comfortable having two different levels of 'error.' It's not clear to me if this would be clarifying or confusing.

Edit: I just re-read this and I don't think it's a good idea

@ryanheise
Copy link

I'll add my 👍 for this feature. I don't see any insurmountable technical challenges in principle, only surface issues such as the verbosity of the type system, and the implications for code migration. Putting that aside, I think a type system could be designed that fits the criteria, and an exception inference algorithm could be made to help developers automatically add the exception type annotations to their functions through a quick fix. This would also help in the case of higher order functions to ensure that the function's exception type is parameterised. If the concern is that it will affect the surface syntax, I would even settle for making this aspect of the type system completely implicit, but still provide the static analysis tool to be able to report when we have unhandled exceptions in our program. The output of the analyser could be cached per module.

That's my technical perspective, but now for a completely different perspective, here's my marketing perspective:

As the marketing says (and rightfully so), null safety kills bugs brought about by null pointers, one of the most common types of bugs that cause production apps to crash. But you know the other most common cause of bugs: Uncaught exceptions! Without any static analysis tools, it is very difficult to track down and correctly handle all of the exception cases appropriately. The marketing machine has put a very positive spin on the null safety feature (and rightfully so), but of course we know there were also negatives: a huge technical effort behind the scenes, and also a very long and painful migration across the ecosystem. But somehow the pain was worth it, and even though I never asked for this feature, I am glad I now have it. Basically, these two features have similar value and a similar marketing story.

With null safety, I appreciate that I can now change the nullability signature of a method and the analyser will automatically report to me the 15 places in our 15,000 line codebase that will be affected by this change and in fact won't let it compile until all possible crash conditions are handled. But we have exactly the same problem with uncaught exceptions. When we upgrade a 3rd party library and the exceptions thrown by an API changes, we have no way to reliably know how to handle the changes required to our catch blocks. So ultimately we find out after releasing to production.

P.S. I agree with @lukepighetti that we don't want two different language features for the purpose of errors. That would be quite the mess. (Plus, returning an error would be even more verbose because it would require manually unwrapping and re-wrapping the error all the way down the stack all the way to the catcher. True exceptions bypass that and jump straight to the catcher.)

@Erhannis
Copy link

Erhannis commented Oct 2, 2021

Just putting in my vote - as a Java programmer, learning Dart, I miss checked exceptions. (I also write Swift.) I no longer have confidence whether any 3rd party function will return normally. A coworker, who switched from Java to Flutter about a year ago, says basically the absence of checked exceptions are the one thing he hates. I wouldn't mind some other solution - union return types, for example - I just want to have some confidence that my code won't frequently crash for some hitherto invisible reason.

@bsutton
Copy link
Author

bsutton commented Oct 2, 2021

@Erhannis
dart-lang/linter#2246

I've proposed a lint that would deliver most of the value.

I think it has a better chance of getting support.

If you like the idea then up vote it

@lukehutch
Copy link

With proper support for union types (#1222), checked exceptions could be returned as part of the return value (as discussed in several previous comments). For example, int.parse(String) could return type int | FormatException.

Pattern matching (#546) is what would make this much cleaner to work with, for example:

final intVal = int.parse(stringVal) match {
   case int v => v;
   case FormatException e => -1;
}

@lukehutch
Copy link

Also probably there should be a shorthand (e.g. ???) that works with union types, and that evaluates to the lefthand operand if its type is not an Exception, or the righthand operand if the type of the lefthand operand is an Exception:

final intVal = int.parse(stringVal) ??? -1;

@ryanheise
Copy link

There still needs to be an idiomatic dart way of doing exceptions, and so whatever solution is adopted, it should be something that can be turned on or off in the static analyser without influencing whether we return or throw an exception.

@lukehutch
Copy link

If union types were supported in Dart, the symbol support already in Dart could be used to propagate "exceptions" (in the form of symbols). It's not the same thing as checked exceptions, but it still forces the caller to handle exceptional cases in clean ways.

e.g. in addition to int.parse (which either returns int or throws an exception) and int.tryParse (which returns int?), something like int.parseOrInvalid could be added, which returns union type int | #invalid. Then everything that uses the return value will have to handle the case of the return value being equal to #invalid.

(The main issue with this is that currently, symbols are only stable within libraries, not across library boundaries.)

@jamesderlin
Copy link

I've raised an issue suggesting that we add a lint rule as a middle ground.

The lint rule would give most of the benefits in that exceptions would be documented without requiring a fairly significant changed to the language.

dart-lang/linter#2246

IMO, I doubt that such a lint would improve anything. You'd still be in a situation where:

A. Every function would need to manually document which uncaught exceptions it (and any functions it calls) throws. That burden is equivalent to having a checked exception specification.

B. People would get lazy and ignore/disable the lint (especially if it's a burden), so the lint would be rarely used and wouldn't be pulling its own weight.

If your goal is for exceptions to be documented, then, as @OlegAlexander suggested, it'd be better to just target that goal directly: automatically document thrown exceptions, similar to how dartdoc generates an "Implementers" list for classes. I think that's the only realistic way for such a thing to not be a burden. dartdoc could, for example, note all exceptions explicitly thrown by each function, generate a call graph, and list all uncaught exceptions from a function and all functions it calls. Some complications:

  • When there are exception hierarchies, you might not want to document all leaves. One heuristic could be to document the nearest parent that itself has public documentation.
  • Wouldn't work for functions not implemented in Dart.
  • Such an exception list could never be exhaustively complete in the presence of higher-order functions or method overrides.
  • Such an exception list should never be used as a binding contract.

I wouldn't actually expect any of that to be done either, but I do think it'd be more realistic. (And it could even be done by some third-party package.)

@bsutton
Copy link
Author

bsutton commented Mar 14, 2023 via email

@ryanheise
Copy link

If your goal is for exceptions to be documented

The primary goal is for exceptions to be "checked" so that I can be told by the compiler when my code was broken by an update.

Whether that goal be achieved via explicit (e.g. annotations or documentation) or implicit (e.g. inference) means is the means to that goal.

Another approach I don't think has been talked about enough is to introduce more stringent guidelines on what constitutes a breaking change for semantic versioning purposes. Quite often, package developers only consider visible API changes (e.g. to the method signature) when they decide to bump the major version, but they don't consider exceptions. Even if we never get checked exceptions in the language or in the linter, it would still be nice to have more stringent guidelines for package developers so that they are more aware of when they should be flagging a release as a breaking change due to hidden changes in exception handling.

@bsutton
Copy link
Author

bsutton commented Mar 14, 2023 via email

@dancojocaru2000
Copy link

It's disappointing that error handling in Dart is bad.

Reading through this issue, one thing that surprisingly came up a lot was "but higher order functions", and, having used Swift, it's puzzling that it was considered such a big problem.

Even if this is rejected, I'll add this here so that people will know that it's definitely possible to have checked exceptions and higher order functions:

func rethrowingFunction(throwingCallback: () throws -> Void) rethrows {
    try throwingCallback()
} 

// Because the callback isn't throwing, rethrowingFunction also isn't throwing
rethrowingFunction({
    print("Hello!")
})

// Because the callback is throwing, calling rethrowingFunction must use the try keyword
try rethrowingFunction({
    throw SomeError
})

A function that rethrows may not throw any exceptions itself, and so it will only throw if the callbacks passed in will throw.

@bernaferrari
Copy link

Swift now has this: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling/#Specifying-the-Error-Type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem state-rejected This will not be worked on
Projects
None yet
Development

No branches or pull requests