sdk
sdk copied to clipboard
Can't catch error on `stdout.writeln`
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.
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)
//cc @brianquinlan
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.
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.
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!
I find it very... unfortunate --- that when doing something sync one suddenly has to deal with essentially async stuff.
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?
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.
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.