skip to content

In Dart, what can a zone carry through zoneValues and intercept through a ZoneSpecification, and when would a server job use them?

level: seniorimportance: nice to knowfreq 16%

answer

  1. context that follows async code
  2. Zone.current[key], else null
  3. unique keys, inherited, shadowable
  4. print, timers, microtasks intercepted
  5. self, parent, zone arguments

basics

~20 s

zoneValues attach read-only context, such as a job id, that every callback in the zone sees via Zone.current[key]. A ZoneSpecification overrides zone operations such as print, timer and microtask creation, callback registration and uncaught-error handling.

solid answer

~40 s

`runZoned(body, zoneValues: {...}, zoneSpecification: ...)` forks a zone. **Zone values** are a map fixed at fork time; code anywhere in that zone's asynchronous extent reads them with `Zone.current[key]`, which returns `null` if no enclosing zone defines the key. Child zones inherit and may shadow them, and keys should be unique objects so libraries do not collide. That makes them a fit for request- or job-scoped context such as a correlation id for logs. A **`ZoneSpecification`** supplies hooks — `print`, `scheduleMicrotask`, `createTimer`, `createPeriodicTimer`, `registerCallback`, `run`, `errorCallback`, `handleUncaughtError`, `fork` — each receiving `self`, a `parent` delegate and the originating `zone`. Uses: prefix every `print` with the job id, count timers, or fake time in tests. `fake_async`, used by flutter_test's widget-test binding, intercepts timers this way.

code

dart · 25 lines
dart
import 'dart:async';

final _jobIdKey = Object();

String? get currentJobId => Zone.current[_jobIdKey] as String?;

Future<void> runJob(String jobId, Future<void> Function() body) {
  return runZoned(
    body,
    zoneValues: {_jobIdKey: jobId},
    zoneSpecification: ZoneSpecification(
      print: (self, parent, zone, line) {
        parent.print(zone, '[job ${zone[_jobIdKey]}] $line');
      },
    ),
  );
}

Future<void> main() async {
  await runJob('nightly-42', () async {
    await Future<void>.delayed(const Duration(milliseconds: 5));
    print('report built'); // [job nightly-42] report built
  });
  print(currentJobId); // null outside the job zone
}

go deeper

for a junior

Know that zones can carry values that follow async code, read with Zone.current[key].

for a middle

Explain fork-time values, parent lookup and shadowing, null for missing keys, and the self/parent/zone arguments of a hook.

for a senior

Use zone values for correlation ids and a print or timer hook for diagnostics, while keeping hooks cheap and real dependencies as parameters.

for a principal

Decide whether ambient zone context is acceptable in a codebase at all, and confine it to cross-cutting concerns such as log correlation and test clocks.

## Zones as asynchronous context A **zone** follows asynchronous work: callbacks registered in a zone — `then` handlers, timer callbacks, stream listeners — run in that same zone later. That makes a zone the natural place to keep context that must survive `await`s without being passed through every function, and to change how some low-level operations behave for one piece of code. You create one with `runZoned(body, {zoneValues, zoneSpecification})` (or `runZonedGuarded`, which adds an error handler), or with the lower-level `Zone.current.fork(...)` followed by `zone.run(...)`. ## Zone values - **Set once, at fork time.** `zoneValues` is a `Map<Object?, Object?>`. You cannot change which object a key maps to, though you can mutate that object. - **Read with the index operator.** `Zone.current[#jobId]` looks the key up in the current zone, then its parent, and so on; it returns `null` if nobody defines it. The result type is `dynamic`, so cast it: `Zone.current[#jobId] as String?`. - **Inherited and shadowable.** A nested zone sees its parents' values and can override a key for its own extent. - **Unique keys.** Any object with sensible `==` and `hashCode` works; a private symbol or a `final _key = Object();` avoids collisions with other libraries. Typical use in a server job: fork one zone per job run with `{#jobId: id}`, and have the logging helper read `Zone.current[#jobId]`. Every log line from that run — including lines written from timers and stream callbacks deep in the call graph — carries the id, and concurrent runs do not trample each other the way a global variable would. ## ZoneSpecification hooks A `ZoneSpecification` overrides zone operations. Its constructor takes optional handlers: | Hook | Intercepts | |---|---| | `handleUncaughtError` | Uncaught asynchronous errors (what `runZonedGuarded` installs) | | `run`, `runUnary`, `runBinary` | Entering the zone to run a callback | | `registerCallback` (and unary/binary forms) | Registering a callback that will run later | | `errorCallback` | Replacing or augmenting an error and stack trace as it is reported | | `scheduleMicrotask` | `scheduleMicrotask` calls | | `createTimer`, `createPeriodicTimer` | `Timer` and `Timer.periodic` creation | | `print` | Calls to the top-level `print` | | `fork` | Creating child zones | Every hook receives three leading arguments, always in this order: 1. **`self`** — the zone whose specification is handling the call; 2. **`parent`** — a `ZoneDelegate` to forward the operation to the parent zone; 3. **`zone`** — the zone where the operation originated. A hook that only decorates forwards to the parent: `parent.print(zone, '[job $id] $line')`. `ZoneSpecification.from(existing, ...)` copies a specification and overrides selected hooks. ## Where interceptors earn their keep - **Logging context:** a `print` hook that prefixes each line with the zone's job id. - **Test clocks:** the `fake_async` package, which flutter_test's widget-test binding uses, runs code in a zone that captures timers and microtasks so a test can advance time instantly. - **Diagnostics:** counting or tracing timers created by a piece of code to find one that is never cancelled. ## Cautions - Hooks run on hot paths; `registerCallback` and `run` fire constantly, so keep them cheap. - Zone values are not a dependency-injection system. Anything a function truly needs should be a parameter; zone values suit cross-cutting context like correlation ids. - Streams run their callbacks in the zone where they are **listened to**, so a value visible where a stream was built may not be visible in its listener's zone.

  • Why is a private object key better than a string key like 'jobId' for zone values?
    Zone values are one lookup chain shared by every library running in that zone. A common string could be defined or read by unrelated code. A private `Object()` or private symbol can only be used by the library that declares it, which also controls who may read the value.
  • Why does a ZoneSpecification hook receive both self and zone?
    `self` is the zone whose specification is running the hook; `zone` is where the operation started, which may be a descendant. Forwarding operations need the originating zone — a microtask delegated to the parent must still run in `zone`, and `fork` must create the child under `zone`.

saying these in an interview costs you the question

  • Zone values can be reassigned from anywhere inside the zone.
  • Zone.current[key] throws when the key is not defined.
  • A ZoneSpecification hook must never delegate to the parent zone.
  • Zone values are a good replacement for passing required parameters.
  • Stream callbacks see the zone values of where the stream was created.