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

A null-aware exception catching expression. #2656

Open
lrhn opened this issue Nov 24, 2022 · 7 comments
Open

A null-aware exception catching expression. #2656

lrhn opened this issue Nov 24, 2022 · 7 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Nov 24, 2022

Sometimes you want to catch an exception and just return null to signal that something went wrong. That's currently cumbersome since catching exceptions can only happen as a statement.

You can write a function like:

R? tryOrNull<R, E>(R Function() action) {
  try {
    return action();
  } on E {
    return null;
  }
}

It suffers from needing two type arguments, where you only want to provide the E one, and you need to introduce a closure when using it, so:

tryOrNull<int, Exception>(() => theCodeToRun(args));

I propose introducing an expression form of try looking like:

try(expression)
try<E>(expression)

The try operator evaluates expression to a value. If expression throws (if a type argument is provided, then only if it throws an E), the try expression evaluates to null, otherwise it evaluates to the result of evaluating expression.

Proposal

Grammar:

primary :: = ...
  | `try' <typeArguments>? '(' expression ')'

It's a compile-time error if there is more than one type argument.

An expression of the form try(e) is equivalent to an expression of the form try<Object>(e), where Object refers to the type from dart:core (whether it's in scope or not). You're allowed to write Object? or dynamic as the type argument, it won't make any difference, the type is only used for subtype checks on values that are non-null.

We won't need to restrict an expression statement from starting with try, since the grammar for try expressions and try statements differ at the second token. The first token after an expression try is ( or <, and it's {` for the statement, so there is no parsing ambiguity.

Semantics

Let e be an expression of the form try<T>(e2) or `try(e2)~.

Static

Static typing of e with context type schema C proceeds as:

  • If e has no type argument, infer a type argument of <Object>, and let T be Object in the following.
  • Infer the type of e with context type schema NON_NULL(C) to a type T2. (We do not enforce the context type, or insert downcasts or implicit coercions here, any actual value will be passed through unchanged.)
  • The static type of e is T2?.

Runtime

Evaluation of e proceeds as follows:

  • Evaluate e2.
  • If evaluation of e2 completes with a 9c/value v, then evaluation of e completes with the value v.
  • If evaluation of e2 throws a error o and stack trace s,
    • If o is a subtype of T, evaluation of e completes with the value null.
    • otherwise evaluation of e throws o with stack trace s.
@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Nov 24, 2022
@eernstg
Copy link
Member

eernstg commented Nov 25, 2022

Interesting! Note that this feature is somewhat similar to the proposals about adding other expressions with control flow, #2025, so it might be good to think about them as group.

@Cat-sushi
Copy link

I would like finally clause, as well.

await mutex.lock();
var ret = try<E>(retrieveData()) finally { mutex.unlock(); };

In comparison with the example below, the type of ret can be inferred.
And, mutex.unlock() is proved even in case of exception other than E, as well as the example below.

await mutex.lock();
RetType? ret;
try {
  ret = retrieveData();
} on E {
  ret = null;
} finally {
  mutex.unlock();
}

@lrhn
Copy link
Member Author

lrhn commented Nov 28, 2022

I like the finally clause. The only problem I see is that the suggested syntax won't allow you a try (e) finally { stmt } which doesn't catch exceptions. You'd have to write that as try<Never>(e) finally {stmt}, which is a little backwards.

Maybe make Never the default catch-type if a finally is supplied, and Object if no finally is supplied. The difference is still a little jarring.

@Cat-sushi
Copy link

I am not a syntax man, so I don't stick to such syntax.

a try (e) finally { stmt } which doesn't catch exceptions.

I'm not sure what does it mean.

await mutex.lock();
var ret = try /* <E> */ (retrieveData()) finally { mutex.unlock(); };

can't be treated as

await mutex.lock();
RetType? ret;
try {
  ret = retrieveData();
} catch (e) {
  ret = null;
} finally {
  mutex.unlock();
}

?

@lrhn
Copy link
Member Author

lrhn commented Nov 28, 2022

The point is that if you only want the finally functionality, and don't want to catch any errors, corresponding to:

await mutex.lock();
RetType ret;
try {
  ret = retrieveData();
} finally {
  mutex.unlock();
}

then you couldn't just do var ret = try (retrieveData()) finally { mutex.unlock(); }, because the definition of try (expr) given above would catch Object. There was no syntax for try(...) without catching, because it wasn't needed.
With finally, it might actually be needed.

So we could change try (expr) to not catch anything, but that's silly because then it doesn't do anything.

That's why I suggested:

try (e)       // Catches any thrown object and converts it to `null`
try<E>(e)  // Catches `E`s and converts them to `null`
try (e) finally {s}  // Catches no objects, executes {s}` afterwards. (Does not promote to nullable).
try<E>(e) finally {S} // catches `E`s and converts them to `null`, executes `{s}` afterwards.

That gives the shortest syntax to the simplest usable behavior, but allows you to explicitly write a catch type to override it.

You can still write try<Never>(e) to do ... well, nothing. Promote to nullable, I guess.
Or try<Object>(e) finally {s} to catch every thrown object and do a finally afterwards.

@Cat-sushi
Copy link

Cat-sushi commented Nov 28, 2022

and don't want to catch any errors

I did wanted catch all errors in my previous example.

I want some syntax not to catch any errors, as well.
But, I think this is another feature request so called "make try able to be evaluated as expression" without introducing default null value, something like #27 and #307

@Jetz72
Copy link

Jetz72 commented Nov 28, 2022

The idea of an expression try/catch came up in #1884, as a shorthand for declaring variables whose initialization processes can fail. Instead of needing to write a whole try block with the declaration outside it so the variable stays in the scope you need it in, you could just declare it and its fallback all in one line: var v = try(getValue()) ?? fallback; instead of

FooType v;
try {
  v = getValue();
}
on Object{
  v = fallback;
}

Having it evaluate to null rather than falling into a subsequent catch expression means you can't extract info from the exception and do anything useful with it (e.g. log a warning). But for cases where you don't need that, it does seem elegant to be able to use the existing null-aware tools to specify the fallback, so I like this idea.

Though if finally is being considered, I'd still like to suggest an optional catch clause to the expression that can use the exception in the fallback. E.g. in try<E>(foo()) catch((e, st) => bar(e)) (not sure about that syntax; ideally the StackTrace could optionally be left off), if foo() throws, use bar(e) as the value instead, where e is the exception (implicitly type E). If foo has static type T, bar (and the overall try/catch expression) should have type T?.

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

4 participants