Please compare the two following examples:
Example 1:
import 'dart:async';
Future<void> main() async {
final c = Completer<void>();
print(1);
c.completeError(Exception("hello"));
print(2);
await c.future;
}
which prints
1
2
Failed to load ".../x_test.dart": hello
Example 2:
import 'dart:async';
Future<void> main() async {
final c = Completer<void>();
print(1);
await (() async => c.completeError(Exception("hello")))();
print(2);
await c.future;
}
which prints
1
Failed to load ".../x_test.dart": hello
I thought that in the second example, the exception should be bound to the future of the Completer and hence also be thrown when awaiting for the future.
Is there a way to defer an exception with a Completer to a later point?
What I see when I run your second example in DartPad is
1
Uncaught Error: Exception: hello
2
Uncaught Error: Exception: hello
The error is uncaught twice. That matches what is actually happening:
You print 1.
You call () async { c.complete(Exception("hello")); }(). This schedules a microtask to
complete the completer's future with the exception. Then the call returns another future and schedules a microtask to complete that future with null.
Then you await the latter future.
Then the first microtask runs and completes the completer's future with an error. At this point there are no listeners on the future, so the error is considered uncaught and reported to the root zone's uncaught async error handler. This prints the first uncaught error line.
Then the second microtask runs and completers the other future. This makes the await complete.
Then you print 2.
Then you await the completer's future. Since the future is already complete, this schedules a microtask report this to the await.
Then that microtask runs and the second await completes by throwing the exception of the completer's future.
That throw is uncaught and is therefore also reported as an uncaught error.
It's no entirely clear what you want. Do you want the completer's future to complete later (if so, how much), or do you just not want the first completion to be considered uncaught?
The latter is easier, just do:
completer..future.catchError((_){})..complete(Exception("hello"));
If you pre-attach an error handler to the future, which handles (by ignoring) the error, then the future won't consider the error unhandled.
(Arguably, it would be nice to have a way to complete a completer with an error, and say up front that it shouldn't be considered unhandled).
Related
So I've read the docs and this example in particular:
runZoned(() {
HttpServer.bind('0.0.0.0', port).then((server) {
server.listen(staticFiles.serveRequest);
});
},
onError: (e, stackTrace) => print('Oh noes! $e $stackTrace'));
I understand the whole zone-local-values thing, and the sort of AOP option of tracing code through a zone - enter/exit.
But other than that, is the code block above any different than:
try {
var server = await HttpServer.bind('0.0.0.0', port);
server.listen(staticFiles.serveRequest);
} catch (e, stackTrace) {
print('Oh noes! $e ${e.stackTrace}');
}
Thanks :)
The runZoned code introduces a new uncaught error handler.
Some asynchronous errors do not happen in a context where they can be thrown to user code. For example, if you create a future, but never await it or listen to it in any other way, and that future then completes with an error, that error is considered "uncaught" or "unhandled". Futures work using callbacks, and there has been no callback provided that it can call.
In that case, the current zone's uncaught error handler is invoked with the error.
Your first example introduces such a handler, which means that if the body of the runZoned call has any uncaught async errors, you'll be told about them.
That includes the future returned by calling then, but also any other asynchronous errors that may happen (if any).
When you await a future, and that future completes with an error, that error is (re)thrown, an can be caught. The try/catch of the second example will catch that error (and any thrown by calling listen, which shouldn't be any as long as server isn't null, which it shouldn't be).
So, the difference is only really in whether uncaught asynchronous errors are handled. (And how any errors in the catch code is propagated, because an error happening during an uncaught-error zone handler will again be uncaught.)
I notices some code that made me think the Exception function call was optional? E.g., do these two lines perform the same function?
throw Exception('oops!');
throw 'oops!'
No.
The former, throw Exception('oops!');, creates a new Exception object using the Exception constructor, then throws that object.
It can be caught by try { ... } on Exception catch (e) { ... }.
The latter, throw 'oops!'; throws the string object.
It can be caught by try { ... } on String catch (e) { ... }.
Generally, you shouldn't be doing either.
If someone made an error, something that would have been nice to catch at compile-time and reject the program, but which happens to not be that easy to detect, throw an Error (preferably some suitable subclass of Error).
Errors are not intended to be caught, but to make the program fail visibly. Some frameworks do catch errors and log them instead. They're typically able to restart the code which failed and carry on, without needing to understand why.
If your code hit some exceptional situation which the caller should be made aware of (and which prevents just continuing), throw a specific subclass of Exception, one which contains the information the caller needs to programmatically handle that situation. Document that the code throws this particular exception. It's really a different kind of return value more than it's an error report. Exceptions are intended to be caught and handled. Not handling an exception is, itself, an error (which is why it's OK for an uncaught exception to also make the program fail visibly).
If you're debugging, by all means throw "WAT!"; all you want. Just remove it before you release the code.
See example below:
import 'dart:async';
Future<void> main(List<String> arguments) async {
try {
await someMethod();
} catch (e, st) {
print("error: ${e}");
print("st : ${st}");
}
}
Future<void> someMethod() async {
final completer = Completer.sync();
_scheduleCompletion(completer);
await completer.future;
}
Future<void> _scheduleCompletion(Completer completer) async {
Future.delayed(Duration(milliseconds: 1), () {
try {
[][0]; // simulating error
completer.complete();
} catch (e, st) {
completer.completeError("error occured", st);
}
});
}
I am receiving following output:
#0 List.[] (dart:core-patch/growable_array.dart:254:60)
#1 _scheduleCompletion.<anonymous closure> (file:///home/work/stuff/projects/dart-async-exceptions/bin/dart_async_exceptions.dart:26:9)
#2 new Future.delayed.<anonymous closure> (dart:async/future.dart:315:39)
#3 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#4 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:395:19)
#5 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:5)
#6 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
As you can see stacktrace does not allow you to see what steps led to error. What I expect is something like:
[anonymous closure, where error actually happens]
[someMethod]
[main]
while instead I see only last part.
I know there is package, called stack_trace, that allows you to "wrap your whole program in Chain.capture and you will be happy". Unfortunately my real-world case is not that simple and I can not simply "wrap my program" in "Chain.capture".
Another reason on not using "Chain.capture" thing is that in some cases it's onError handler gets even worse stacktrace vs. simple try/catch block .
Also I would like to understand why this is happening and what should I do to fix my program so part of valuable trace is not lost.
UPDATE (based on Irn's answer):
Let's re-write someMethod not to use completer, but rather call another async method:
Future<void> someMethod() async {
await someOtherMethod();
}
Future<void> someOtherMethod() async {
throw "another error";
}
In that case I will have following trace:
#0 someOtherMethod (file:///home/work/stuff/projects/dart-async-exceptions/bin/dart_async_exceptions_original.dart:24:3)
<asynchronous suspension>
#1 someMethod (file:///home/work/stuff/projects/dart-async-exceptions/bin/dart_async_exceptions_original.dart:19:3)
<asynchronous suspension>
#2 main (file:///home/work/stuff/projects/dart-async-exceptions/bin/dart_async_exceptions_original.dart:5:5)
<asynchronous suspension>
As you see it contains all invocations main -> someMethod -> someOtherMethod, that allows you to narrow down the issue. As I understand in that case we should also lose everything as we are scheduling calls on event loop. But we see everything works as expected. Why is that?
About stack_trace package: I would love to use it, but I made small experiment and looks like sometimes it provides worse trace then the one from try/catch. But that is topic for another question ;)
The short answer is that you can't trace a stack which doesn't exist.
Dart's asynchronous code is event loop based.
When you do await someFuture, your function really sets up a callback on that future, using Future.then, and then it returns. That unwinds the stack leading up to setting that callback, and eventually the stack all the way back to the event loop.
(The return only happens when you evaluate the await, the someFuture expression is invoked immediately, and if that manages to throw before doing something asynchronous, then you will get the stack trace containing the stack prior to the await returning).
At a later time, the event loop triggers and calls the callback, and the asynchronous computation continues from there.
The original stack no longer exists at that point, which is why you see a stack trace leading back to an event loop event (here, a timer, which the VM implements by sending port events at specific times). The stack you see is the only stack which exists at the point you ask for it.
You can use package:stack_trace to capture a stack trace at the point where you schedule a callback, and then the stack trace package will remember that stack trace until the callback is run, and attempt to combine it with the stack traces reported (or captured) during the callback.
Since you make lots of callbacks, but don't usually need to see that many stack traces, it has a significant performance impact to capture stacks at every callback.
Best practice is of course to always attach an error handler to a Future using catchError() (or using await and try/catch). But suppose I forgot, or suppose that this error is serious enough that we want it to crash the entire application (as we could do with synchronous exceptions), or that I want to log the error, or report it to some service like Crashlytics to make me aware of my sins.
Dart's Futures are practically the same as JavaScript's Promises. In NodeJS, we can attach a handler to the global unhandledRejection event to add custom behaviour.
Does Dart offer something similar? I looked through the async and Future documentation, but couldn't find anything relevant.
Take a look at the runZonedGuarded static method. It will executing a given method in its own Zone which makes it possible to attach a method to handle any uncaught errors.
I have made a simple example here which shows what happens if a async error are throw without any handling of the error:
import 'dart:async';
import 'dart:io';
void main() {
runZonedGuarded(program, errorHandler);
}
Future<void> program() async {
final file = File('missing_file.txt');
await file.openRead().forEach(print);
}
void errorHandler(Object error, StackTrace stack) {
print('OH NO AN ERROR: $error');
}
Which returns:
OH NO AN ERROR: FileSystemException: Cannot open file, path = 'missing_file.txt'...
I have two functions
callee() async {
// do something that takes some time
}
caller () async {
await callee();
}
In this scenario, caller() waits till callee() finishes. I don't want that. I want caller() to complete right after invoking callee(). callee() can complete whenever in the future, I don't care. I just want to start it just like a thread and then forget about it.
Is this possible?
When you call the callee function, it returns a Future. The await then waits for that future to complete. If you don't await the future, it will eventually complete anyway, but your caller function won't be blocked on waiting for that. So, you can just do:
caller() {
callee(); // Ignore returned Future (at your own peril).
}
If you do that, you should be aware of what happens if callee fails with an error. That would make the returned future complete with that error, and if you don't listen on the future, that error is considered "uncaught". Uncaught errors are handled by the current Zone and the default behavior is to act like a top-level uncaught error which may kill your isolate.
So, remember to handle the error.
If callee can't fail, great, you're done (unless it fails anyway, then you'll have fun debugging that).
Actually, because of the risk of just forgetting to await a future, the highly reocmmended unawaited_futures lint requires that you don't just ignore a returned future, and instead wants you to do unawaited(callee()); to signal that it's deliberate. (The unawaited function can be imported from package:meta and will be available from the dart:async library in SDK version 2.14).
The unawaited function doesn't handle errors though, so if you can have errors, you should do something more.
You can handle the error locally:
caller() {
callee().catchError((e, s) {
logErrorSomehow(e, s);
});
}
(Since null safety, this code only works if the callee() future has a nullable value type. From Dart 2.14, you'll be able to use callee().ignore() instead, until then you can do callee().then((_) => null, onError: (e, s) => logErrorSomehow(e, s)); instead.)
or you can install an error handling zone and run your code in that:
runZoned(() {
myProgram();
}, onError: logErrorSomehow);
See the runZoned function and it's onError parameter.
Sure, just omit await. This way callee() is called immediately and when an async operation is called the call will be scheduled in the event queue for later execution and caller() is continued immediately afterwards.
This isn't like a thread though. As mentioned processing is enqueued to the event queue which means it won't be executed until the current task and all previously enqueued tasks are completed.
If you want real parallel execution you need to utilize isolates.
See also
https://www.dartlang.org/articles/event-loop/
https://api.dartlang.org/stable/1.16.1/dart-isolate/dart-isolate-library.html
https://pub.dartlang.org/packages/isolate