Skip to content

Async stack unwinding does not properly unwind a stack through Future<List<T>> get wait #59730

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

Closed
stephane-archer opened this issue Dec 16, 2024 · 9 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)

Comments

@stephane-archer
Copy link

Future<void> convert(String file) async {
  await Future.delayed(Duration(seconds: 10));
  throw "hello";
}

Future<void> main() async {
  var files = ['foo', 'bar'];
  var futures = files.map((file) {
    return convert(file);
  });
  try {
    await futures.wait;
  } catch (e) {
    print(e);
  }
}

When executing this code an Uncaught exception happens at the end of the function but all futures have been await using `await futures.wait;

I think Future<List<T>> get wait is not doing his job properly

See screenshots for execution:

Screenshot 2024-12-16 at 22 22 26 Screenshot 2024-12-16 at 22 22 32 Screenshot 2024-12-16 at 22 22 49 Screenshot 2024-12-16 at 22 23 05 Screenshot 2024-12-16 at 22 23 17
@dart-github-bot
Copy link
Collaborator

Summary: User reports futures.wait doesn't catch exceptions thrown by individual futures in a Iterable<Future>. The provided code demonstrates an unhandled exception despite using await futures.wait.

@dart-github-bot dart-github-bot added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Dec 16, 2024
@mraleph mraleph changed the title I think Future<List<T>> get wait is not doing his job properly Async stack unwinding does not properly unwind a stack through Future<List<T>> get wait Dec 17, 2024
@mraleph
Copy link
Member

mraleph commented Dec 17, 2024

The exception itself is caught, but the debugger is treating it as uncaught. That happens quite often in async code because the only reliable way to figure out if exception will bubble through or not is to run Future implementation code. We have some heuristics - but these occasionally fail like here.

@stephane-archer
Copy link
Author

Suggestions for Improvement:

  • Track the "Await Stack": Like the "Call Stack" in synchronous code, tracking the "Await Stack" could help the debugger determine whether the exception is truly uncaught.
  • Refine Heuristics: Updating heuristics to better handle common async patterns could reduce false positives.
  • Debugger Context: The debugger could display clearer messages when exceptions are flagged due to heuristic limitations, reducing confusion.

@mraleph what are your thoughts on these propositions?

@lrhn lrhn removed the triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. label Dec 17, 2024
@mraleph
Copy link
Member

mraleph commented Dec 18, 2024

@mraleph what are your thoughts on these propositions?

That they are generated by some LLM and as such neither wrong nor right. Just saying words which kinda make sense together, but not truly showing any knowledge or comprehension.

The actual fix is here: https://dart-review.googlesource.com/c/sdk/+/401062

@stephane-archer
Copy link
Author

@mraleph LLM rephrases my words because I'm not good at writing and wrote something too long.

I have limited knowledge of the issue, I was expecting the debugger to be able to know the "Await Stack" like regular synchronous code.

I'm glad you were able to find a solution. What approach did you use? Refining the Heuristics?

@mraleph
Copy link
Member

mraleph commented Dec 19, 2024

What approach did you use?

The solution was to slightly rewrite wait extension and annotate some parts of it with vm:awaiter-link pragma so that unwinder could see through it.

Dart Future objects don't form a neat stack - they form a data flow graph held together by callbacks which transform and forward values. That's what makes this complicated. Both for debugging and stack trace reporting purposes we try to pretend that there is some sort of awaiter stack, but it is just not entirely true.

@stephane-archer
Copy link
Author

@mraleph, thanks for finding a solution so rapidly.

I have a limited understanding of this subject, but because the Dart runtime can associate the right future/exception with the right try-catch, would it make sense to share this flow graph with the debugger?
It seems like the runtime and the debugger track things differently.
Is this because the debugger (not made for async code?) wants to see a classic "call stack" while the runtime only knows who is waiting for whom (flow graph)?
What allows the runtime to create this flow graph but not the debugger?

@mraleph
Copy link
Member

mraleph commented Feb 17, 2025

@stephane-archer Sorry for the delayed response.

The problem is that there is no true "flow graph" - at least not in a pure form. There is a maze of boxes (Future objects) which are connected through callbacks. These callbacks take a value from one box (Future which just completed), transform it (e.g. apply then), and put it into some another box (next Future). You can't really reliably know what happens with a value you put into one Future without running a bunch of Dart code which implements async machinery internals and that code will invoke all kinds of user written code as well.

That's why async unwinding is so complicated and unreliable - the async stack does not exist in "declarative" form (e.g. as a list of frames or even as a graph of connected computations), it is very imperative. You need to run the code to determine what will happen. To reliably figure out where exception lands (and whether it will be caught or not) you need to actually run the code and see. Async unwinding tries to compensate for that - but it might fail.

That being said: both debugger logic and StackTrace.current logic are backed by the same machinery. If you see a good (not truncated) stack trace in StackTrace.current then debugger will also use the same stack trace.

@stephane-archer
Copy link
Author

@mraleph Thanks for taking the time to explain this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)
Projects
None yet
Development

No branches or pull requests

4 participants