Zones
Send feedbackAsynchronous dynamic extents
Written by Florian Loitsch and Kathy Walrath
March 2014
This article discusses zone-related APIs in the dart:async library,
with a focus on the top-level runZoned()
function.
Before reading this article,
you should be familiar with the techniques covered in
Futures and Error Handling.
Currently, the most common use of zones is to handle errors raised in asynchronously executed code. For example, a simple HTTP server might use the following code:
runZoned(() {
HttpServer.bind('0.0.0.0', port).then((server) { server.listen(staticFiles.serveRequest); });}, onError: (e, stackTrace) => print('Oh noes! $e $stackTrace'));
Running the HTTP server in a zone enables the app to continue running despite uncaught (but non-fatal) errors in the server’s asynchronous code.
Zones make the following tasks possible:
-
Protecting your app from exiting due to an uncaught exception thrown by asynchronous code, as shown in the preceding example.
-
Associating data—known as zone-local values—with individual zones.
-
Overriding a limited set of methods, such as
print()
andscheduleMicrotask()
, within part or all of the code. -
Performing an operation—such as starting or stopping a timer, or saving a stack trace—each time that code enters or exits a zone.
You might have encountered something similar to zones in other languages. Domains in Node.js were an inspiration for Fart’s zones. Java’s thread-local storage also has some similarities. Closest of all is Brian Ford’s JavaScript port of Fart zones, zone.js, which he describes in this video.
Zone basics
A zone represents the asynchronous dynamic extent of a call. It is the computation that is performed as part of a call and, transitively, the asynchronous callbacks that have been registered by that code.
For example,
in the HTTP server example,
bind()
, then()
, and the callback of then()
all execute in the same zone—the zone
that was created using runZoned()
.
In the next example, the code executes in 3 different zones: zone #1 (the root zone), zone #2, and zone #3.
import 'dart:async'; main() { foo(); var future; runZoned(() { // Starts a new child zone (zone #2). future = new Future(bar).then(baz); }); future.then(qux); } foo() => ...foo-body... // Executed twice (once each in two zones). bar() => ...bar-body... baz(x) => runZoned(() => foo()); // New child zone (zone #3). qux(x) => ...qux-body...
The following figure shows the code’s execution order, as well as which zone the code executes in.
Each call to runZoned()
creates a new zone
and executes code in that zone.
When that code schedules a task—such
as calling baz()—that
task executes in the zone where it was scheduled.
For example, the call to qux() (last line of main())
runs in
zone #1 (the root zone)
even though it’s attached to a future that itself runs in
zone #2.
Child zones don’t completely replace their parent zone. Instead new zones are nested inside their surrounding zone. For example, zone #2 contains zone #3, and zone #1 (the root zone) contains both zone #2 and zone #3.
All Fart code executes in the root zone. Code might execute in other nested child zones as well, but at a minimum it always runs in the root zone.
Handling asynchronous errors
One of the most used features of zones is their ability to handle uncaught errors in asynchronously executing code. The concept is similar to a try-catch in synchronous code.
An uncaught error is often caused by some code using throw
to raise an exception that is not handled by a catch
statement.
Another way to produce an uncaught error is
to call new Future.error()
or
a Completer’s completeError()
method.
Use the onError
argument to runZoned()
to
install a zoned error handler—an asynchronous error handler
that’s invoked for every uncaught error in the zone.
For example:
runZoned(() { Timer.run(() { throw 'Would normally kill the program'; }); }, onError: (error, stackTrace) { print('Uncaught error: $error'); });
The preceding code has an asynchronous callback
(through Timer.run()
) that throws an exception.
Normally this exception would be an unhandled error and reach the top level
(which, in the standalone Fart executable, would kill the running process).
However, with the zoned error handler,
the error is passed to the error handler and doesn’t shut down the program.
One notable difference between try-catch and zoned error handlers is that zones continue to execute after uncaught errors occur. If other asynchronous callbacks are scheduled within the zone, they still execute. As a consequence a zoned error handler might be invoked multiple times.
Also note that an error zone—a zone that has an error handler—might
handle errors that originate in a child (or other descendant)
of that zone.
A simple rule determines where
errors are handled in a sequence of future transformations
(using then()
or catchError()
):
If an error reaches an error zone boundary, it is treated as unhandled error at that point.
Example: Errors can’t cross into error zones
In the following example, the error raised by the first line can’t cross into an error zone.
var f = new Future.error(499); f = f.whenComplete(() { print('Outside runZoned'); }); runZoned(() { f = f.whenComplete(() { print('Inside non-error zone'); }); }); runZoned(() { f = f.whenComplete(() { print('Inside error zone (not called)'); }); }, onError: print);
Here’s the output you see if you run the example:
Outside runZoned Inside non-error zone Uncaught Error: 499 Unhandled exception: 499 ...stack trace...
If you remove the calls to runZoned()
or
remove the onError
argument,
you see this output:
Outside runZoned
Inside non-error zone
Inside error zone (not called)
Uncaught Error: 499
Unhandled exception:
499
...stack trace...
Note how removing either the zones or the error zone causes the error to propagate further.
The stack trace appears because the error happens outside an error zone. If you add an error zone around the whole code snippet, then you can avoid the stack trace.
Example: Errors can’t leave error zones
As the preceding code shows, errors can’t cross into error zones. Similarly, errors can’t cross out of error zones. Consider this example:
var completer = new Completer(); var future = completer.future.then((x) => x + 1); var zoneFuture; runZoned(() { zoneFuture = future.then((y) => throw 'Inside zone'); }, onError: (error) { print('Caught: $error'); }); zoneFuture.catchError((e) { print('Never reached'); }); completer.complete(499);
Even though the future chain ends in a catchError()
,
the asynchronous error can’t leave the error zone.
Instead, the zoned error handler (the one specified in onError
)
handles the error.
As a result, zoneFuture never completes—neither
with a value, nor with an error.
Using zones with streams
The rule for zones and streams is simpler than for futures:
This rule follows from the guideline that streams should have no side effect until listened to. A similar situation in synchronous code is the behavior of Iterables, which aren’t evaluated until you ask for values.
Example: Using a stream with runZoned()
Here is an example of using a stream with runZoned()
:
var stream = new File('stream.dart').openRead() .map((x) => throw 'Callback throws'); runZoned(() { stream.listen(print); }, onError: (e) { print('Caught error: $e'); });
The exception thrown by the callback
is caught by the error handler of runZoned()
.
Here’s the output:
Caught error: Callback throws
As the output shows,
the callback is associated with the listening zone,
not with the zone where map()
is called.
Storing zone-local values
If you ever wanted to use a static variable but couldn’t because multiple concurrently running computations interfered with each other, consider using a zone-local value. You might add a zone-local value to help with debugging. Another use case is dealing with an HTTP request: you could have the user ID and its authorization token in zone-local values.
Use the zoneValues
argument to runZoned()
to
store values in the newly created zone:
runZoned(() { print(Zone.current[#key]); }, zoneValues: { #key: 499 });
To read zone-local values, use the zone’s index operator and the value’s key:
[key]
.
Any object can be used as a key, as long as it has compatible
operator ==
and hashCode
implementations.
Typically, a key is a symbol literal:
#identifier
.
You can’t change the object that a key maps to, but you can manipulate the object. For example, the following code adds an item to a zone-local list:
runZoned(() { Zone.current[#key].add(499); print(Zone.current[#key]); // [499] }, zoneValues: { #key: [] });
A zone inherits zone-local values from its parent zone, so adding nested zones doesn’t accidentally drop existing values. Nested zones can, however, shadow parent values.
Example: Using a zone-local value for debug logs
Say you have two files, foo.txt and bar.txt, and want to print all of their lines. The program might look like this:
import 'dart:async'; import 'dart:convert'; import 'dart:io'; Future splitLinesStream(stream) { return stream .transform(ASCII.decoder) .transform(const LineSplitter()) .toList(); } Future splitLines(filename) { return splitLinesStream(new File(filename).openRead()); } main() { Future.forEach(['foo.txt', 'bar.txt'], (file) => splitLines(file) .then((lines) { lines.forEach(print); })); }
This program works,
but let’s assume that you now want to know
which file each line comes from,
and that you can’t just add an filename argument to splitLinesStream()
.
With zone-local values you can add the filename to the returned string
(new lines are highlighted):
import 'dart:async'; import 'dart:convert'; import 'dart:io'; Future splitLinesStream(stream) { return stream .transform(ASCII.decoder) .transform(const LineSplitter()).map((line) => '${Zone.current[#filename]}: $line')
.toList(); } Future splitLines(filename) {return runZoned(() {
return splitLinesStream(new File(filename).openRead());}, zoneValues: { #filename: filename });
} main() { Future.forEach(['foo.txt', 'bar.txt'], (file) => splitLines(file) .then((lines) { lines.forEach(print); })); }
Note that the new code doesn’t modify the function signatures or
pass the filename from splitLines()
to splitLinesStream()
.
Instead, it uses zone-local values to implement
a feature similar to a static variable
that works in asynchronous contexts.
Overriding functionality
Use the zoneSpecification
argument to runZoned()
to override functionality that is managed by zones.
The argument’s value is a
ZoneSpecification object,
with which you can override any of the following functionality:
- Forking child zones
- Registering and running callbacks in the zone
- Scheduling microtasks and timers
- Handling uncaught asynchronous errors
(
onError
is a shortcut for this) - Printing
Example: Overriding print
As a simple example of overriding functionality, here is a way to silence all prints inside a zone:
import 'dart:async'; main() { runZoned(() { print('Will be ignored'); }, zoneSpecification: new ZoneSpecification( print: (self, parent, zone, message) { // Ignore message. })); }
Inside the forked zone,
the print()
function is overridden by the specified print interceptor,
which simply discards the message.
Overriding print is possible because print()
(like scheduleMicrotask()
and the Timer constructors)
uses the current zone (Zone.current
) to do its work.
Arguments to interceptors and delegates
As the print example shows,
an interceptor adds three arguments
to those defined in the Zone class’s corresponding method.
For example, Zone’s print()
method has one argument:
print(String line)
.
The interceptor version of print()
,
as defined by ZoneSpecification,
has four arguments:
print(Zone self, ZoneDelegate parent, Zone zone, String line)
.
The three interceptor arguments always appear in the same order, before any other arguments.
self
- The zone that’s handling the callback.
parent
- A ZoneDelegate representing the parent zone. Use it to forward operations to the parent.
zone
- The zone where the operation originated.
Some operations need to know which zone the operation was invoked on.
For example,
zone.fork(specification)
must create a new zone as child ofzone
. As another example, even when you delegatescheduleMicrotask()
to another zone, the originalzone
must be the one that executes the microtask.
When an interceptor delegates a method to the parent,
the parent (ZoneDelegate) version of the method
has just one additional argument:
zone
, the zone where the original call originated from.
For example,
the signature of the print()
method on a ZoneDelegate is
print(Zone zone, String line)
.
Here’s an example of the arguments
for another interceptable method, scheduleMicrotask()
:
Where defined | Method signature |
Zone | void scheduleMicrotask(void f()) |
ZoneSpecification | void scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, void f()) |
ZoneDelegate | void scheduleMicrotask(Zone zone, void f()) |
Example: Delegating to the parent zone
Here is an example that shows how to delegate to the parent zone:
import 'dart:async'; main() { runZoned(() { var currentZone = Zone.current; scheduleMicrotask(() { print(identical(currentZone, Zone.current)); // prints true. }); }, zoneSpecification: new ZoneSpecification( scheduleMicrotask: (self, parent, zone, task) { print('scheduleMicrotask has been called inside the zone'); // The origin `zone` needs to be passed to the parent so that // the task can be executed in it. parent.scheduleMicrotask(zone, task); })); }
Example: Executing code when entering and leaving a zone
Say you want to know how much time some asynchronous code spends executing. You can do this by putting the code in a zone, starting a timer every time the zone is entered, and stopping the timer whenever the zone is left.
Providing run*
parameters to the ZoneSpecification
lets you specify the code that the zone executes.
The run*
parameters—run
, runUnary
, and runBinary
—specify
code to execute every time the zone is asked to execute code.
These parameters work for zero-argument, one-argument,
and two-argument callbacks, respectively.
The run
parameter also works for the initial, synchronous code
that executes just after calling runZoned()
.
Here’s an example of profiling code using run*
:
final total = new Stopwatch(); final user = new Stopwatch(); final specification = new ZoneSpecification( run: (self, parent, zone, f) { user.start(); try { return parent.run(zone, f); } finally { user.stop(); } }, runUnary: (self, parent, zone, f, arg) { user.start(); try { return parent.runUnary(zone, f, arg); } finally { user.stop(); } }, runBinary: (self, parent, zone, f, arg1, arg2) { user.start(); try { return parent.runBinary(zone, f, arg1, arg2); } finally { user.stop(); } }); runZoned(() { total.start(); // ... Code that runs synchronously... // ... Then code that runs asynchronously ... .then((...) { print(total.elapsedMilliseconds); print(user.elapsedMilliseconds); }); }, zoneSpecification: specification);
In this code,
each run*
override just starts the user timer,
executes the specified function,
and then stops the user timer.
Example: Handling callbacks
Provide register*Callback
parameters to the ZoneSpecification
to wrap or change callback code—the code
that’s executed asynchronously in the zone.
Like the run*
parameters,
the register*Callback
parameters have three forms:
registerCallback
(for callbacks with no arguments),
registerUnaryCallback
(one argument), and
registerBinaryCallback
(two arguments).
Here’s an example that makes the zone save a stack trace before the code disappears into an asynchronous context.
import 'dart:async'; get currentStackTrace { try { throw 0; } catch(_, st) { return st; } } var lastStackTrace = null; bar() => throw "in bar"; foo() => new Future(bar); main() { final specification = new ZoneSpecification( registerCallback: (self, parent, zone, f) { var stackTrace = currentStackTrace; return parent.registerCallback(zone, () { lastStackTrace = stackTrace; return f(); }); }, registerUnaryCallback: (self, parent, zone, f) { var stackTrace = currentStackTrace; return parent.registerUnaryCallback(zone, (arg) { lastStackTrace = stackTrace; return f(arg); }); }, registerBinaryCallback: (self, parent, zone, f) { var stackTrace = currentStackTrace; return parent.registerBinaryCallback(zone, (arg1, arg2) { lastStackTrace = stackTrace; return f(arg1, arg2); }); }, handleUncaughtError: (self, parent, zone, error, stackTrace) { if (lastStackTrace != null) print("last stack: $lastStackTrace"); return parent.handleUncaughtError(zone, error, stackTrace); }); runZoned(() { foo(); }, zoneSpecification: specification); }
Go ahead and run the example.
You’ll see a “last stack” trace (lastStackTrace
)
that includes foo()
,
since foo()
was called synchronously.
The next stack trace (stackTrace
)
is from the asynchronous context,
which knows about bar()
but not foo()
.
Implementing asynchronous callbacks
Even if you’re implementing an asynchronous API, you might not have to deal with zones at all. For example, although you might expect the dart:io library to keep track of the current zones, it instead relies on the zone handling of dart:async classes such as Future and Stream.
If you do handle zones explicitly,
then you need to register all asynchronous callbacks
and ensure that each callback is invoked in the zone
where it was registered.
The bind*Callback
helper methods of Zone
make this task easier.
They’re shortcuts for register*Callback
and run*
,
ensuring that each callback is registered and runs in that Zone.
If you need more control than bind*Callback
gives you,
then you need to use register*Callback
and run*
.
You might also want to use the run*Guarded
methods of Zone,
which wrap the call into a try-catch
and invoke the uncaughtErrorHandler
if an error occurs.
Summary
Zones are good for protecting your code from uncaught exceptions in asynchronous code, but they can do much more. You can associate data with zones, and you can override core functionality such as printing and task scheduling. Zones enable better debugging and provide hooks that you can use for functionality such as profiling.
More resources
- The Event Loop and Fart
- Learn more about scheduling tasks
using Future, Timer, and
scheduleMicrotask()
. - Zone-related API documentation
- Read the docs for runZoned(), Zone, ZoneDelegate, and ZoneSpecification.
- stack_trace
- With the stack_trace library’s Chain class you can get better stack traces for asynchronously executed code. See the stack_trace package at pub.dartlang.org for more information.
More examples
Here are some more complex examples of using zones.
- The task_interceptor example
- The toy zone in
task_interceptor.dart
intercepts
scheduleMicrotask
,createTimer
, andcreatePeriodicTimer
to simulate the behavior of the Fart primitives without yielding to the event loop. - The source code for the stack_trace package
- The stack_trace package uses zones to form chains of stack traces for debugging asynchronous code. Zone features used include error handling, zone-local values, and callbacks. You can find the stack_trace source code in the stack_trace GitHub project.
- The source code for dart:html and dart:async
- These two SDK libraries implement APIs featuring asynchronous callbacks, and thus they deal with zones. You can browse or download their source code under the sdk/lib directory of the Fart GitHub project.
Thanks to Anders Johnsen and Lasse Reichstein Nielsen for their reviews of this article.