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

Inline catching #4205

Open
Protonull opened this issue Dec 13, 2024 · 12 comments
Open

Inline catching #4205

Protonull opened this issue Dec 13, 2024 · 12 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@Protonull
Copy link

This is a request for Zig-style inline catching, which would allow something like the following:

final int example = int.parse("123") catch 0;

Which would then obviate the need for specific APIs like:

final int example = int.tryParse("123") ?? 0;

The problem is that not everything has a premade API that returns null if an error occurred. In fact, there may be some use cases, such as config parsing, where someone may wish to retain null as a valid return type distinct from there having been an error. And if you're someone who cares about finality (that once a variable is set, it cannot be changed), then your options are rather limited.

For example, the following is not possible:

final int example;
try {
    example = int.parse("123");
}
catch (_) {
	example = 0; // Error: The final variable 'example' can only be set once.
}
print("Example: $example");

The only way that I've found to do this is via:

final int example = ((){
    try {
        return int.parse("123");
    }
    catch (_) {
        return 0;
    }
})();
print("Example: $example");

Which is far from ideal.

@Protonull Protonull added the feature Proposed language feature that solves one or more problems label Dec 13, 2024
@julemand101
Copy link

Something I don't really like is that catch will by default catching any kind of thrown object which includes Error objects which are documented to be not be catch since those are serious issues you, as a developer, should have prevented in the first place.

So because of that, the suggested syntax would most often end up looking like:

final int example = int.parse("123") catch on Exception 0;

I feel the readability ends up being less clear when this is needed. And because of that, I think most people would end up just skipping the on Exception part and be burnt when important errors like OutOfMemoryError, NoSuchMethodError and StackOverflowError are just hidden away in a simple single expression.

I therefore like the suggested syntax. But my problems with it are not really directly related to the syntax itself but more the overall design of handling thrown objects in Dart and the consequences of making it too easy.

(and yes, I know we already have the problem with the existing syntax since I every day see people doing catch (_) { in their code and forgetting about adding on Exception. So we already have the problem. I am just worried about making the problem worse by making it even more hidden that an Error also got caught).

@Protonull
Copy link
Author

That's very true. In principle, this suggestion is about letting users quickly and easily recover from small, anticipated errors: you probably don't want to be recovering from an OOM error when all you wanted to do was parse an integer. That said, I actually quite like the catch on Exception 0 syntax in your example. Though, would it catch a throw "Something went wrong!";?

And while this is already an issue, there's the complication of not knowing what to specify as the catch-type since Dart doesn't require nor even allow functions to be defined with what exceptions it may throw (like Java), so it's not something a tab-completer / the analyser can help with. And third-party libraries can be very hit-or-miss with their documentation.

One possible solution is to not allow inline-catches to catch any Error types unless they're expressly defined (so a thrown OutOfMemoryError would catch on a catch on Error 0;, but not on a catch 0;), but this would introduce different behaviours for different kinds of catches, which is not ideal. But this does feel more like an error-handling issue in general.

@lrhn
Copy link
Member

lrhn commented Dec 13, 2024

I'd make it on Exception => 0. But then it starts looking like a pattern.

Maybe change case to catch and you have switch (e) { catch Exception e => log(e).then(0) }.

Idea: Any switch statement or expression can add one or more catch patternClause: body or catch pattern => expression entries.
If they do, and the switch expression throws, those catch clauses can catch the thrown error and respond to it.

We can let switch expressions with only catch clauses be non-exhaustive. They evaluate to the matched value then, as if they ended with var v => v. (Probably still want them to be exhaustive if they start matching values.)

(Might want to do something with finally too, otherwise people will start implementing it using catch.
Problem with expression-finally is that isn't really an expression, it's something you do on the way out.)

@tatumizer
Copy link

tatumizer commented Dec 13, 2024

@lrhn:
FWIW, I like the idea, but unfortunately, some current limitations of darts prevent it from fully realizing its potential.

  1. There are two colors of "switch" (in dart, everything comes in two colors :-). In a statement-switch catch becomes "case catch" :-(

  2. In the expression-switch, we can write only a single expression in catch Exception e => expr, but while catching exceptions, it's often necessary to write into a log file (or do something, not necessarily asynchronous) so everyone will be forced to use IIFE. Dart badly needs expression blocks! :-)

Q: have you ever considered supporting the form switch (final v = expr) {...} ?

@lrhn
Copy link
Member

lrhn commented Dec 13, 2024

In a statement-switch catch becomes "case catch" :-(

Doesn't have to. One keyword is enough to know that a switch entry starts there, and catch will do as long as it can't start a statement.

but while catching exceptions, it's often necessary to ...

... use statements. That's not just while catching exceptions. Expression blocks might be the solution for this too.

Q: have you ever considered supporting the form switch (final v = expr) {...} ?

Maybe! What would it do? 🤔
(But declarations as expressions is definitely something I'd want in general, #1420.)

@tatumizer
Copy link

Maybe! What would it do?

It's just a way to make it look a bit more consistent with the general practice of writing _ => expr at the end:

var a = switch (final v=expr) {
  catch Exception e => 0,
  _ => v // to avoid writing "var v = > v"
}; 

Just looks a bit more familiar. Or not?


About "finally". It's probably not very relevant to the case. When we invoke just a single expression, it shouldn't leave any observable side effects, especially those requiring cleanup.
Also, a normal try - catch - finally construct works in such a way that "finally" gets executed no matter what (even if you catch exception and invoke "continue" for some outer block, "finally" still gets executed). The syntax of try-catch-finally attempts to make this point clear. It's difficult to express this otherwise, and since it's unnecessary anyway, the best option is to to nothing about it IMO.

@tatumizer
Copy link

@lrhn:
I asked this question before, but I don't remember the answer (or maybe there was no answer?): is it possible to optimize the exception handling mechanism so that the stack trace doesn't get filled if no one needs it?
E.g. if the exception of a certain kind is clearly caught by the caller who doesn't care about the stack trace, then the latter doesn't get filled. (The mechanism has to consider whether the exception gets rethrown, and whether whoever is handling this rethrown exception expects a stack trace, etc).

Further. some exceptions may inherit a (hypothetical) CallerMustHandle type of Exception, which could force the user (via a warning) to use an (exhaustive) switch expression, as proposed above.
This would obviate the need to use the (non-existent) union types for error handling.

WDYT?

@lrhn
Copy link
Member

lrhn commented Dec 15, 2024

It's hypothetically possible to recognize that the handler on the stack does not catch a stack trace, and does not rethrow. If that is the case, there is no need to create a StackTrace object.

I added "hypothetically" because it doesn't work as easily with async, because async is push, not pull, so error handling doesn't know who it is sending an async error to, if that listener even exists yet. You can store an error in a Future indefinitely, and then choose to see the stack trace much later.

For synchronous code, all general code tends to capture stack traces, because someone might want it. Or you want to log it. Or you threw an Error which captured the stack as Error.stackTrace.
So the cases where there is something to save are fairly rare, and it does require an extra runtime check to see if the current catch handler even cares about stack traces. Probably negligible for performance, maybe shareable to avoid extra code bloat.

(If I designed this from scratch today, I'd probably only have Error objects capturing stack traces, and no stack trace along with Exceptions at all. You should no more need a stack trace for an Exception than you should for a return value. If you need to know where an exception was thrown, attach a debugger.)

I don't think introducing special kinds of exceptions that must be handled (Java checked exceptions, basically) is going to fly. Probably doable using annotations and a lint, though.

@tatumizer
Copy link

tatumizer commented Dec 15, 2024

The use cases for "traceless" exceptions are exactly those where people are currently requesting union types (I figure this is the main use case for union types). Introducing the second color of the error-handling mechanism in the form of union types would further confuse the user, so I'm fantasizing about re-purposing the existing throw-catch mechanism for that - assuming it wouldn't incur a disproportional performance penalty associated with the creation of the stack trace.
(I know, union types have other uses - I'm not questioning their validity).

If you believe capturing stack trace is unnecessary for Exceptions, you can introduce a TracelessException which won't capture it, and use it in the above scenario. It will create a StackTrace object, but it will be empty (or contain a minimal amount of information). Then, there will be no problem with async code.

@tenhobi
Copy link

tenhobi commented Dec 16, 2024

I don't think introducing special kinds of exceptions that must be handled (Java checked exceptions, basically) is going to fly. Probably doable using annotations and a lint, though.

I think there is no Dart lint right now that would do this, but DCM has this rule for "ckeched exceptions" https://dcm.dev/docs/rules/common/handle-throwing-invocations

@stan-at-work
Copy link

This is nice

@He-Pin
Copy link

He-Pin commented Jan 4, 2025

Seems bad , as int.parse("123") catch 0; reads as catching 0 not a fallback 0

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
Projects
None yet
Development

No branches or pull requests

7 participants