language icon indicating copy to clipboard operation
language copied to clipboard

Syntax catching multiple exceptions in a single on block

Open jonasfj opened this issue 7 years ago • 12 comments

In many cases I only care what type of exception I'm getting. I don't care about the exception at-most I might want to do a e.toString().

Example:

for (int i = 0; i < retries; i++) {
  try {
    return networkOperation().timeout(Duration(seconds: 5));
  } on SocketException, TimeoutException {
    continue;
  }
}

This isn't critical at all, I was just surprised it didn't work already.

It's trivial to duplicate the block handling the exception, or write a function that accepts a list of types to ignore.

Please close if there is some complication to this that I don't see :)

jonasfj avatar Nov 28 '18 16:11 jonasfj

Being able to catch multiple exception in one block is really useful in my opinion to avoid duplication of code or noise on error management

jaumard avatar Dec 06 '18 07:12 jaumard

If you look at this more generally, a catch clause:

  • Tests an object against some type (the on clause).
  • If the object has that type, optionally bind a new variable for the object (the catch clause).
  • Conditional executes some code where that variable is in scope (the catch block).

That's really similar to how pattern matching works in languages that have that. Most of those languages have some kind of | pattern that lets you concatenate multiple patterns together.

We're considering adding tuples to Dart, and those almost always lead to some kind of pattern matching syntax in order to destructure the fields back out of the tuple. If we do that, one solution for the problem here would be to extend to catch clauses to allow arbitrary patterns. If we have some kind of "or" pattern, that would then let you have a single catch clause that matches objects of multiple types.

munificent avatar Dec 10 '18 18:12 munificent

Related link.

omidraha avatar Aug 01 '20 18:08 omidraha

One issue with allowing multiple on types is that it makes it harder to type the catch (e) variable. We can either disallow having a catch clause if you have more than one on type, or we can find some least upper bound of the on types to use for the e variable. (A general least upper bound function in an interface based language isn't well-defined, there might not be one least upper bound.) We definitely don't want to introduce union types for this.

lrhn avatar Dec 18 '20 10:12 lrhn

or we can find some least upper bound of the on types to use for the e variable.

I think that would feel comfortable for most authors since we hit it with other patterns too.

natebosch avatar Jan 04 '21 20:01 natebosch

disallow having a catch clause if you have more than one on type

That seems like a reasonable tradeoff.

Any progress on this language feature? It seems that the discussion is already dead.

marianhlavac avatar Jan 22 '22 10:01 marianhlavac

Any progress on this language feature? It seems that the discussion is already dead.

No progress (it's not a particularly burning issue), but we are working on pattern matching again, it's possible that we'll incorporate some pattern stuff into on clauses, which might cover this use case. No promises, because, again, it's not a super high priority, but it would be nice.

munificent avatar Jan 25 '22 19:01 munificent

One issue with allowing multiple on types is that it makes it harder to type the catch (e) variable. We can either disallow having a catch clause if you have more than one on type, or we can find some least upper bound of the on types to use for the e variable. (A general least upper bound function in an interface based language isn't well-defined, there might not be one least upper bound.) We definitely don't want to introduce union types for this.

Could something like:

try {
  doSomething();
} on ExceptionA, ExceptionB catch (e) {
  fail(e);
}

be syntactic sugar for:

try {
  doSomething();
} catch (e) {
  if (e is ExceptionA || e is ExceptionB) {
    fail(e);
  } else {
    rethrow;
  }
}

? In the above case, e is already determined to be a common base type of ExceptionA and ExceptionB.

Edit: Ignore me, I am wrong. I accidentally did on Exception catch (e) { ... } when I tried it and then mistook e's type as Exception to be the common base type.

jamesderlin avatar May 25 '22 16:05 jamesderlin

Perhaps you could leave it to the user to specify their preferred common parent for the variable when they name it in the catch clause, if there's more than one type in the on clause. E.g. on ExceptionA, ExceptionB catch(Exception e) {. All types in the on clause would need to be subtypes of whichever type is specified in the catch clause. Leaving the type out could default to Object.

Jetz72 avatar May 25 '22 16:05 Jetz72

What is the method resolution order for the type system? If Type A is parent to Type B then on Type A will catch Type B, and so on. Some types will then be ignored if their ancestor is already defined, or would the on keyword first defer to explicit type matching.

sashkaizen avatar Jul 03 '25 09:07 sashkaizen

Considering that Dart has pattern matching now, this issue can be looked at from a different perspective. A try-catch block could act as a switch statement, destructuring the error object in case of an error. Conceptually like this:

try {
  maybeFailingOperation();
} catch (e) {
  switch (e) {
    case ExceptionA e || WrapperException(inner: ExceptionA e):
      handleExceptionA(e);
    case ExceptionB() || ExceptionC():
      handleExceptionBandC();
    default:
      rethrow;
  }
}

The syntax for this could look like this:

try {
  maybeFailingOperation();
} on ExceptionA e || WrapperException(inner: ExceptionA e) {
  handleExceptionA(e);
} on ExceptionB() || ExceptionC() {
  handleExceptionBandC();
} on _ {
  rethrow;
}

Of course the old catch-way has to be preserved. But it is pretty simple to distinguish between a clause using catch and one using pattern matching:

try {
  maybeFailingOperation();
} on ExceptionA e {
  handleExceptionA(e);
} on ExceptionB catch(e) {
  handleExceptionB(e);
}

Functionally both are the same in this example. The catch clause would maybe transition to being deprecated at some point.

Dampfwalze avatar Dec 09 '25 17:12 Dampfwalze

Defintely a place where a refutable pattern fits right in.

Strawman:

try { 
  // ...
} case Exception e { 
  // ...
} case Error e when (e is! UnimplementedError) {
  // ...
}

The when clause may need to be parenthesized to avoid the {...} block becoming ambiguous or just hard to parse. (Just like initializer lists can't contain unparenthesized function expressions because it makes a following {...} a problem, we don't want a sequence of <expression> <block> to be a valid.)

Using case instead of on avoids the ambiguity of on SomeType which, as a pattern, is a constant pattern comparing to the Type object for SomeType, and in the existing syntax it means the same as Exception _.

The new syntax also does not give access to the stack trace, so you may still need

} case ExceptionA(isBad: false) catch (e, s) {
  logWithStack(e, s);
}

Alternatively, and maybe foolishly, we could check if the pattern is a record pattern with two positional fields, and if it is, we apply it to (error, stack) instead of just the error value. (Probably a bad idea. Refutable patterns are interpreted based on the matched value type, trying to derive a matched value type from them is going in the wrong direction. That's usually only done for declaration patterns.)

I doubt anyone will ever need to look at the stack trace in the matching, so letting you do catch (e, s) case (PatternForE, PatternForS) isn't actually useful.

lrhn avatar Dec 10 '25 10:12 lrhn