sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Can't catch error on `stdout.writeln`

Open jensjoha opened this issue 1 year ago • 5 comments

I have this code

import "dart:io";

void main() {
  try {
    stdout.writeln("Line #1");
    stdout.writeln("Line #2");
    stdout.writeln("Line #3");
    stdout.writeln("Line #4");
    stdout.writeln("Line #5");
  } catch(e) {
    stderr.writeln("Error: $e");
  }
}

If I pipe the thing to head -n1 (which presumably closes stdout after the first line) I get this error which isn't caught by the try:

$ out/ReleaseX64/dart t.dart | head -n1
Line #1
Unhandled exception:
FileSystemException: writeFrom failed, path = '' (OS Error: Broken pipe, errno = 32)
#0      _RandomAccessFile.writeFromSync (dart:io/file_impl.dart:905:7)
#1      _StdConsumer.addStream.<anonymous closure> (dart:io/stdio.dart:309:15)
#2      _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#3      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#4      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#5      _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19)
#6      _StreamController._add (dart:async/stream_controller.dart:658:7)
#7      _StreamController.add (dart:async/stream_controller.dart:606:5)
#8      _StreamSinkImpl.add (dart:io/io_sink.dart:154:17)
#9      _IOSinkImpl.write (dart:io/io_sink.dart:287:5)
#10     _IOSinkImpl.writeln (dart:io/io_sink.dart:307:5)
#11     _StdSink.writeln (dart:io/stdio.dart:342:11)
#12     main ([...]/t.dart:7:12)
#13     _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#14     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

(Apart from the catch not working it's also interesting that it's the call on line 7 (which prints Line #3) that makes it go boom).

Probably related to https://github.com/dart-lang/sdk/issues/48501 but given that this - at least from my side - doesn't do any async stuff it seems sufficiently different to me to warrant a new issue.

The workaround from that issue with using runZonedGuarded does seem to work though:

import "dart:async";
import "dart:io";

void main() {
  runZonedGuarded(() {
    stdout.writeln("Line #1");
    stdout.writeln("Line #2");
    stdout.writeln("Line #3");
    stdout.writeln("Line #4");
    stdout.writeln("Line #5");
  }, (e, _) {
    stderr.writeln("Error: $e");
  });
}
$ out/ReleaseX64/dart t2.dart | head -n1
Line #1
Error: FileSystemException: writeFrom failed, path = '' (OS Error: Broken pipe, errno = 32)

All of this is at current tip-of-tree (f20cd26533aa9ab012fcb069a1cd8dfa17924098) btw.

jensjoha avatar Feb 14 '24 09:02 jensjoha

So even worse runZonedGuarded doesn't actually really work --- if you've printed something before it seems to (sometimes) fail:

import 'dart:async';
import 'dart:io';

void main(List<String> args) {
  stdout.writeln("hello 1");
  runZonedGuarded(() {
    stdout.writeln("hello 2");
    stdout.writeln("hello 3");
    stdout.writeln("hello 4");
    stdout.writeln("hello 5");
  }, (e, _) {
    stderr.writeln("Error: $e");
  });
}

still fail:

$ out/ReleaseX64/dart t2.dart | head -n1
Line #1
Unhandled exception:
FileSystemException: writeFrom failed, path = '' (OS Error: Broken pipe, errno = 32)
#0      _RandomAccessFile.writeFromSync (dart:io/file_impl.dart:905:7)
#1      _StdConsumer.addStream.<anonymous closure> (dart:io/stdio.dart:309:15)
#2      _rootRunUnary (dart:async/zone.dart:1415:13)
#3      _RootZone.runUnaryGuarded (dart:async/zone.dart:1597:7)
#4      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#5      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#6      _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19)
#7      _StreamController._add (dart:async/stream_controller.dart:658:7)
#8      _StreamController.add (dart:async/stream_controller.dart:606:5)
#9      _StreamSinkImpl.add (dart:io/io_sink.dart:154:17)
#10     _IOSinkImpl.write (dart:io/io_sink.dart:287:5)
#11     _IOSinkImpl.writeln (dart:io/io_sink.dart:307:5)
#12     _StdSink.writeln (dart:io/stdio.dart:342:11)
#13     main.<anonymous closure> ([...]/t2.dart:7:12)
#14     _rootRun (dart:async/zone.dart:1399:13)
#15     _CustomZone.run (dart:async/zone.dart:1301:19)
#16     _runZoned (dart:async/zone.dart:1804:10)
#17     runZonedGuarded (dart:async/zone.dart:1792:12)
#18     main ([...]/t2.dart:6:3)
#19     _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#20     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

and -n2 and -n3 only sometimes:

$ out/ReleaseX64/dart t2.dart | head -n2
Line #1
Line #2
$ out/ReleaseX64/dart t2.dart | head -n2
Line #1
Line #2
Unhandled exception:
FileSystemException: writeFrom failed, path = '' (OS Error: Broken pipe, errno = 32)
#0      _RandomAccessFile.writeFromSync (dart:io/file_impl.dart:905:7)
#1      _StdConsumer.addStream.<anonymous closure> (dart:io/stdio.dart:309:15)
#2      _rootRunUnary (dart:async/zone.dart:1415:13)
#3      _RootZone.runUnaryGuarded (dart:async/zone.dart:1597:7)
#4      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#5      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#6      _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19)
#7      _StreamController._add (dart:async/stream_controller.dart:658:7)
#8      _StreamController.add (dart:async/stream_controller.dart:606:5)
#9      _StreamSinkImpl.add (dart:io/io_sink.dart:154:17)
#10     _IOSinkImpl.write (dart:io/io_sink.dart:287:5)
#11     _IOSinkImpl.writeln (dart:io/io_sink.dart:308:5)
#12     _StdSink.writeln (dart:io/stdio.dart:342:11)
#13     main.<anonymous closure> ([...]/t2.dart:9:12)
#14     _rootRun (dart:async/zone.dart:1399:13)
#15     _CustomZone.run (dart:async/zone.dart:1301:19)
#16     _runZoned (dart:async/zone.dart:1804:10)
#17     runZonedGuarded (dart:async/zone.dart:1792:12)
#18     main ([...]/t2.dart:6:3)
#19     _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#20     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
$ out/ReleaseX64/dart t2.dart | head -n3
Line #1
Line #2
Line #3
$ out/ReleaseX64/dart t2.dart | head -n3
Line #1
Line #2
Line #3
Unhandled exception:
FileSystemException: writeFrom failed, path = '' (OS Error: Broken pipe, errno = 32)
#0      _RandomAccessFile.writeFromSync (dart:io/file_impl.dart:905:7)
#1      _StdConsumer.addStream.<anonymous closure> (dart:io/stdio.dart:309:15)
#2      _rootRunUnary (dart:async/zone.dart:1415:13)
#3      _RootZone.runUnaryGuarded (dart:async/zone.dart:1597:7)
#4      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#5      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#6      _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19)
#7      _StreamController._add (dart:async/stream_controller.dart:658:7)
#8      _StreamController.add (dart:async/stream_controller.dart:606:5)
#9      _StreamSinkImpl.add (dart:io/io_sink.dart:154:17)
#10     _IOSinkImpl.write (dart:io/io_sink.dart:287:5)
#11     _IOSinkImpl.writeln (dart:io/io_sink.dart:307:5)
#12     _StdSink.writeln (dart:io/stdio.dart:342:11)
#13     main.<anonymous closure> ([...]/t2.dart:10:12)
#14     _rootRun (dart:async/zone.dart:1399:13)
#15     _CustomZone.run (dart:async/zone.dart:1301:19)
#16     _runZoned (dart:async/zone.dart:1804:10)
#17     runZonedGuarded (dart:async/zone.dart:1792:12)
#18     main ([...]/t2.dart:6:3)
#19     _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#20     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

jensjoha avatar Feb 14 '24 10:02 jensjoha

//cc @brianquinlan

a-siva avatar Feb 14 '24 23:02 a-siva

This is a general usability issue with dart:io APIs (also sockets)

Our dart:io APIs are implemented in a way that makes adding/writing to stream sinks not report errors. Instead the close() will return a future with the error. But of course you may not want to close it immediately after writing something (especially in stdout). That's why the I/O APIs also expose a .done getter returning the same Future object as the close() will return.

So for your example to not crash with an uncaught exception, you have to

main() {
  // Discard errors from writing to stdout.
  // Without this any error writing to stdout will result in an uncaught exception.
  stdout.done.catchError((e) {});

  stdout.write();
  ...
}

Now one could write a helper abstraction to deal with this, maybe even provide a Future based writing API.

mkustermann avatar Feb 15 '24 11:02 mkustermann

Probably related to https://github.com/dart-lang/sdk/issues/48501 but given that this - at least from my side - doesn't do any async stuff it seems sufficiently different to me to warrant a new issue.

This is the same issue. I came to the same conclusion as martin, either catch this on stdin.done, or immediately call flush after the write, and catch it on the future returned from flush.

christopherfujino avatar Feb 15 '24 20:02 christopherfujino

I have a change under review that documents the behavior that @mkustermann referred to in https://github.com/dart-lang/sdk/issues/54911#issuecomment-1945945319.

The approach that I'm suggesting is:

final sink = File('/tmp').openWrite(); // Can't write to /tmp
sink.done.ignore();
sink.write("This is a test");
try {
  // If one of these isn't awaited, then errors will pass silently!
  await sink.flush();
  await sink.close();
} on FileSystemException catch (e) {
  // Handle the error.
}

But I might also suggest a construction like for developers who need more timely feedback about failures:

Exception error?;
final sink = File('/tmp').openWrite(); // Can't write to /tmp
sink.done.catchError((e) { error = e });

while (error == null) {
  sink.writeln('Logging <something>');
}

if (error == null) {
  try {
    await sink.flush();
    await sink.close();
  } on FileSystemException catch (e) {
    error = e;
  }
}

if (error != null) {
  // Handle the error here.
}

I'd be happy to get feedback!

brianquinlan avatar Feb 17 '24 02:02 brianquinlan

I find it very... unfortunate --- that when doing something sync one suddenly has to deal with essentially async stuff.

jensjoha avatar Feb 20 '24 07:02 jensjoha

I find it very... unfortunate --- that when doing something sync one suddenly has to deal with essentially async stuff.

A stdout.writeln() is an async operation (the kernel buffer of the pipe may be full, so a write would be blocking, but we don't block the dart program, instead we queue it up, and later on when kernel tells us it's ready to accept more data we write it to the pipe - at which point an error can occur (e.g. pipe closed)).

print() on the other hand is synchronous.

Our Stdout class is entirely asynchronous. Maybe what you're asking for is a synchronous API for stdout/stderr?

mkustermann avatar Feb 20 '24 08:02 mkustermann

It isn't marked as async -- so the function call is synchronous -- which makes me have certain expectations. I do see (now) the documentation (copied from IOSink) saying something about seeing some other methods for errors --- but frankly I've never seen that before.

But yes, probably I would like a synchronous API for stdout/stderr --- especially because that's really what I thought I had.

jensjoha avatar Feb 20 '24 08:02 jensjoha

Reformulating the example as:

import "dart:io";

void main() {
  stdout.done.ignore();  // If there were more output, it would make sense to stop writing if done completes.
  try {
    stdout.writeln("Line #1");
    stdout.writeln("Line #2");
    stdout.writeln("Line #3");
    stdout.writeln("Line #4");
    stdout.writeln("Line #5");
  } catch (e) {
    stderr.writeln("Error: $e");
  }
}

fixes the issue. This is a common pattern when using IOSink. I'm going to make an attempt a re-writing the IOSink docs.

brianquinlan avatar Mar 14 '24 22:03 brianquinlan