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

Dart2js throw/catch bug #53082

Open
nex3 opened this issue Jul 31, 2023 · 8 comments
Open

Dart2js throw/catch bug #53082

nex3 opened this issue Jul 31, 2023 · 8 comments
Assignees
Labels
area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. P2 A bug or feature request we're likely to work on type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) web-dart2js

Comments

@nex3
Copy link
Member

nex3 commented Jul 31, 2023

The following code handles nested throws and catches incorrectly in dart2js:

Future<void> main() async {
  try {
    try {
      await Future(() => throw FormatException("oh no"));
    } catch (error) {
      print("catch 1: $error");
      try {
        throw "aw beans";
      } on String catch (error) {
        print("catch 2: $error");
      }
    }
  } catch (error) {
    print("catch 3: $error");
  }
}

The Dart VM correctly prints:

catch 1: FormatException: oh no
catch 2: aw beans

When compiled with dart2js and run in Node.js or a browser, this incorrectly prints:

catch 1: FormatException: oh no
catch 3: FormatException: oh no
nex3 added a commit to sass/dart-sass that referenced this issue Jul 31, 2023
@lrhn lrhn added web-dart2js type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. labels Aug 1, 2023
@biggs0125 biggs0125 self-assigned this Aug 1, 2023
@biggs0125
Copy link

Thanks for filing this and attaching a simple reproduction!

The issue here is a scoping issue in the way we're handling a non-async catch block within an async catch block. More specifically it's causing references to the inner catch variable to be overwritten by the references to the outer one. Therefore when we try to do the String type test, it's being checked against the FormatException rather than the "aw beans" string. The type test fails and we fall through to the outer try/catch instead.

I have a fix in progress. But if you need a temporary workaround the bug can be avoided by making the inner try/catch async:

Future<void> main() async {
  try {
    try {
      await Future(() => throw FormatException("oh no"));
    } catch (error) {
      print("catch 1: $error");
      try {
        await Future.value(); // Add a no-op await to make this async.
        throw "aw beans";
      } on String catch (error) {
        print("catch 2: $error");
      }
    }
  } catch (error) {
    print("catch 3: $error");
  }
}

@sigmundch sigmundch added the P2 A bug or feature request we're likely to work on label Aug 4, 2023
@nex3
Copy link
Member Author

nex3 commented Aug 4, 2023

This also happens if null or undefined is thrown—that's forbidden in Dart, but valid in JS, and so probably shouldn't crash the app.

@lrhn
Copy link
Member

lrhn commented Aug 4, 2023

That's actually an interesting case.
A null value thrown through JavaScript should be uncatchable by Dart code, since all Dart catch clauses have an implicit on Object if they have no explicit on type.
That check is probably not implemented, assuming that you cannot throw null anyway, but it needs to be performed to avoid binding null to a non-nullable catch variable.

@nex3
Copy link
Member Author

nex3 commented Aug 5, 2023

If that ends up throwing a TypeError, how can Dart code handle arbitrary errors thrown from JavaScript? Would it be possible to specify that the null gets caught as something different (maybe the TypeError itself)?

@lrhn
Copy link
Member

lrhn commented Aug 5, 2023

We generally don't specify anything about values which are not Dart values, or about things that cannot happen within the language, it's up to the individual backends to make any value appear to be a Dart value (subtype of Object or null), and manage what happens if something that wouldn't be sound in Dart happens anyway

I think it is reasonable for web compilers to treat any JS object which is not a Dart object, as being instances of JSObject, and let any thrown null value be catchable by a catch with no on type, by converting it to another object.
Maybe the old NullThrownError could be reintroduced for that purpose, or a type error of some sort.
(Or just make it uncatchable, if there is going to be a == null check in every catch anyway, to convert the value to the correct error, we can make it do anything in that branch. It's not like throwing null is that useful.)

@nex3
Copy link
Member Author

nex3 commented Aug 7, 2023

Another option, if you want to avoid == null for every catch, would be to allow on Null catch or on Object? catch at the language level even though there's no way to throw null in pure Dart.

@lrhn
Copy link
Member

lrhn commented Aug 8, 2023

There are lots of places in the SDK, and the async API, where we assume that a caught value is not null, so we'd have to do something if we actually catch null.

We could allow on Null catch (e) or on Object? catch (e) and make e nullable there, while keeping the default of on Object if you don't write anything. Then a magically thrown null will be uncaught by } catch (e) { ... }, but not completely uncatchable.

But it probably means that everybody will start to defensively write on Object? and then start arguing that Completer.completeError should accept null as argument. I don't want to go in that direction.
(If we had allowed throwing null from the start, I would also have had no problems with allowing it, but we're dug in to the "thrown objects are Object" now, and changing that will be breaking. And for no good reason on native, where you cannot throw null.)

I'd prefer that on the web, we either just don't catch null at all, or if we catch it, we convert it to an internale _NullThrownError or type error before exposing it to Dart code.

@nex3
Copy link
Member Author

nex3 commented Aug 17, 2023

I'd prefer that on the web, we either just don't catch null at all, or if we catch it, we convert it to an internale _NullThrownError or type error before exposing it to Dart code.

The latter would be acceptable. The former would make it extremely hard to work with JS code that involves callbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. P2 A bug or feature request we're likely to work on type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) web-dart2js
Projects
None yet
Development

No branches or pull requests

4 participants