dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.21k stars 1.57k forks source link

Stacktrace can ignore user-owned file when using tearoff #56918

Open FMorschel opened 3 days ago

FMorschel commented 3 days ago

Repro:

import 'dart:async';

void main() {
  var controller = StreamController<int>();
  var controller2 = StreamController<int>();
  controller2.stream.listen((v) => controller.add(v));
  controller2.add(42);
  controller.close();
  controller2.add(42);
}

This code throws an error:

Connecting to VM Service at ws://127.0.0.1:34837/-D1zY7tPPbM=/ws
Connected to the VM Service.
Unhandled exception:
Bad state: Cannot add event after closing
#0      _StreamController.add (dart:async/stream_controller.dart:605:24)
#1      main.<anonymous closure> (package:bug/bug.dart:6:47)
#2      _RootZone.runUnaryGuarded (dart:async/zone.dart:1609:10)
#3      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:366:11)
#4      _DelayedData.perform (dart:async/stream_impl.dart:542:14)
#5      _PendingEvents.handleNext (dart:async/stream_impl.dart:647:11)
#6      _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:618:7)
#7      _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#8      _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#9      _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#10     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5)

Exited (255).

Focus on line 1 (not 0) where it tells me the file, function, line and offset of where the error was thrown.

If you use a tear off:

import 'dart:async';

void main() {
  var controller = StreamController<int>();
  var controller2 = StreamController<int>();
  controller2.stream.listen(controller.add);
  controller2.add(42);
  controller.close();
  controller2.add(42);
}

This is the output:

Connecting to VM Service at ws://127.0.0.1:35306/udzDEf1Qq0U=/ws
Connected to the VM Service.
Unhandled exception:
Bad state: Cannot add event after closing
#0      _StreamController.add (dart:async/stream_controller.dart:605:24)
#1      _RootZone.runUnaryGuarded (dart:async/zone.dart:1609:10)
#2      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:366:11)
#3      _DelayedData.perform (dart:async/stream_impl.dart:542:14)
#4      _PendingEvents.handleNext (dart:async/stream_impl.dart:647:11)
#5      _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:618:7)
#6      _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#7      _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#8      _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#9      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5)

Exited (255).

I have no more reference as to where this happened on my code. I think this should be better handled for better DX. I was struggling for more than half an hour on my Flutter project with many different files and pages when I realized where this could be happening. The uncaught exceptions also triggers inside the framework code and there is no way for you to go back to your own code.

dart-github-bot commented 3 days ago

Summary: The stack trace in Dart's uncaught exceptions does not include the user's code when using a tear-off function, making it difficult to pinpoint the source of the error. This issue hinders debugging, especially in larger projects with multiple files.

lrhn commented 3 days ago

That looks correct.

The controller2 is not a synchronous controller, so the call to the function argument to controller2.listen happens in a later microtask. At that point there is nothing on the stack except the internal Stream code that tries to call that function.

Then it calls the function, which is the tear-off controller1.add, which is exactly what shows up in the stack trace. There is no code from main on the stack at this point, so the stack trace is correct.

In the first example, the argument function was (x) { controller1.add(x); }, not controller.add1. That adds one extra stack frame when called, which shows the location of controller.add(x) in main as that position that calls contoller1.add.

Errors that happen in asynchronous code do not have the original stack any more, that's just how event loops work.

FMorschel commented 3 days ago

I know this is the current behaviour. Sorry if I was not clear on my request.

I'd like to ask if there is any way for the call stack to know about the call being a tear off (at least when developing). If this is possible, we could possibly show more information to the programmer in cases like the above so it's easier to find errors even in asynchronous code.

lrhn commented 3 days ago

I guess could be technically possible for an instance member tear-off to record where in the code the tear-off occurred, at least in development mode. It will still be equal (==) to another tear-off of the same function from the same object that happens in another place, but could maybe look different in stack traces.

FMorschel commented 3 days ago

Just to be clear, for the above case as an example, are you talking about controller1.add knowing where it got used or controller2.stream.listen knowing it received a tear off and where?

Just because both could help here but they would show slightly different things in the stack trace I believe.

lrhn commented 2 days ago

I'm talking about controller.add knowing where it was torn off, so that can show up in stack traces.

I doubt the caller itself will know where the function came from, anything special had to happen in the method.

I don't know how the tear-off works. If there is no code in it, and calling it directly invokes the torn off method, there is nothing on the stack to recognize that it was involved. If it contains any code, even if just a jmp to the real method, it should be possible to do something else in development mode, like changing jmp to call+ret, and have a stack entry.

(But I'll stop guessing now and leave this to people who actually know what the VM is doing.)