Dart 63 🎯 Zone Execution Contexts (Zone): Custom Error Zones and Context Binding
A Zone is an execution context that wraps asynchronous callbacks and intercepts the events that happen inside them. The zone can catch the errors the callback throws, the uncaught errors the future rejects, the timers the callback schedules, and the print calls the callback makes. The zone is the mechanism the frameworks use to bind the context across the async boundaries: the Flutter’s error reporting, the server’s request context, and the test’s fake async are the zone-based. This chapter covers the zone’s API, the fork, the error handling, and the patterns.
Key point: A zone does not run the code. It wraps the callbacks that run inside the code. The runZoned executes the body in the new zone, and every async callback the body schedules runs in the same zone. The zone is the ambient context the async cannot otherwise carry.
Why zones exist
The async context problem. An asynchronous callback does not carry the context of the code that scheduled it. The Future.then callback runs later, on the event loop, and the local variables of the scheduling function are the gone. The zone is the mechanism that carries the context: the Zone.current is the ambient, and the callbacks capture the zone they were scheduled in.
The error-handling problem. An uncaught error in the async callback is the hard to catch. The try/catch around the Future does not catch the error the callback throws asynchronously. The zone’s handleUncaughtError is the mechanism that catches the async error.
The framework problem. The Flutter, the AngularDart (the historical), and the server frameworks use the zone to bind the request context. The runZonedGuarded catches the errors, and the Zone.current[#key] reads the context.
The testing problem. The fake_async package uses the zone to intercept the timers and the microtasks. The test advances the clock, and the zone’s createTimer returns the fake.
The tracing problem. The zone can intercept the print, the scheduleMicrotask, and the createTimer. The tracing library uses the interception to record the events.
The performance problem. The zone adds the overhead to the every async operation. The runZoned is the cheap, and the custom zone with the many handlers is the expensive.
The migration problem. The Zone.current is the ambient, and the runZoned is the nesting. The newer code uses the AsyncLocal or the package:zone alternatives, and the migration is the ongoing.
a. The zone API
The Zone is the abstract class. The Zone.current is the current, and the runZoned runs the body in the new.
The current zone
void main() {
print(Zone.current); // The output: the _RootZone
}
The Zone.current returns the current zone. The root zone is the default.
The runZoned
void main() {
runZoned(() {
print(Zone.current); // The output: the _CustomZone
});
}
The runZoned runs the body in the new zone, and the Zone.current inside returns the new.
The fork
The zone.fork creates the child zone with the handlers.
void main() {
final zone = Zone.current.fork(
specification: ZoneSpecification(
print: (self, parent, zone, line) {
parent.print(zone, '[LOG] $line');
},
),
);
zone.run(() {
print('Hello'); // The output: the [LOG] Hello
});
}
The fork creates the child, and the run runs the body.
The ZoneSpecification
The specification has the handlers for the zone’s events.
| The handler | The event |
|---|---|
handleUncaughtError | The uncaught error |
run | The sync body |
runUnary | The unary callback |
runBinary | The binary callback |
registerCallback | The callback registration |
registerUnaryCallback | The unary |
registerBinaryCallback | The binary |
errorCallback | The error callback |
scheduleMicrotask | The microtask |
createTimer | The timer |
createPeriodicTimer | The periodic |
print | The print |
fork | The fork |
The runZonedGuarded
The runZonedGuarded is the convenience for the error handling.
void main() {
runZonedGuarded(() {
Future.delayed(Duration.zero, () {
throw StateError('The async error');
});
}, (error, stack) {
print('Caught: $error');
});
}
The runZonedGuarded runs the body and the error handler catches the async error.
The zone values
The zoneValues is the map of the values.
void main() {
runZoned(() {
final requestId = Zone.current[#requestId];
print(requestId); // The output: the 'abc-123'
}, zoneValues: {
#requestId: 'abc-123',
});
}
The zoneValues is the context, and the Zone.current[#key] reads the value.
The API table
| The element | The purpose |
|---|---|
Zone.current | The current zone |
runZoned | The run in the new |
runZonedGuarded | The run with the error |
zone.fork | The create the child |
zone.run | The run the body |
zoneValues | The context |
ZoneSpecification | The handlers |
b. The error handling
The zone’s handleUncaughtError is the mechanism for the async errors.
The runZonedGuarded
void main() {
runZonedGuarded(() {
Future.delayed(Duration.zero, () {
throw StateError('The async error');
});
}, (error, stack) {
print('Caught: $error');
print(stack);
});
}
The handler catches the error the future rejects and the stack.
The custom handleUncaughtError
void main() {
final zone = Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stack) {
print('The zone caught: $error');
// The parent is the not called.
},
),
);
zone.run(() {
Future.delayed(Duration.zero, () {
throw StateError('The error');
});
});
}
The handleUncaughtError is the custom, and the parent is the not called.
The try/catch versus the zone
The try/catch catches the sync error, and the zone catches the async.
// The try/catch does not catch this:
try {
Future.delayed(Duration.zero, () {
throw StateError('The async');
});
} catch (e) {
// The not reached.
}
// The zone catches:
runZonedGuarded(() {
Future.delayed(Duration.zero, () {
throw StateError('The async');
});
}, (error, stack) {
// The reached.
});
The Flutter integration
void main() {
runZonedGuarded(() {
runApp(MyApp());
}, (error, stack) {
// The report to the crash service.
FlutterError.onError = (details) {
// The Flutter errors.
};
});
}
The runZonedGuarded wraps the runApp, and the handler reports the errors.
The server integration
void main() {
runZonedGuarded(() {
final server = HttpServer.bind('localhost', 8080);
server.listen((request) {
// The request handling.
});
}, (error, stack) {
// The log.
});
}
The runZonedGuarded wraps the server, and the handler logs the errors.
The error table
| The mechanism | The catches |
|---|---|
The try/catch | The sync |
The runZonedGuarded | The async |
The handleUncaughtError | The custom |
The FlutterError.onError | The Flutter |
The PlatformDispatcher.onError | The engine |
c. The context binding
The zoneValues is the context, and the Zone.current[#key] reads the value.
The request context
void main() {
runZoned(() {
final requestId = Zone.current[#requestId];
handleRequest(requestId);
}, zoneValues: {
#requestId: 'abc-123',
});
}
The requestId is the ambient, and the handleRequest reads it.
The log context
void main() {
runZoned(() {
log('The message'); // The output: the [abc-123] The message
}, zoneValues: {
#requestId: 'abc-123',
});
}
void log(String message) {
final requestId = Zone.current[#requestId] ?? '-';
print('[$requestId] $message');
}
The log reads the context, and the message is the prefixed.
The nesting
void main() {
runZoned(() {
runZoned(() {
final outer = Zone.current[#outer];
final inner = Zone.current[#inner];
print('$outer $inner'); // The output: the 'A B'
}, zoneValues: {#inner: 'B'});
}, zoneValues: {#outer: 'A'});
}
The zones nest, and the values are the combined.
The parent access
void main() {
runZoned(() {
final value = Zone.current.parent?[#key];
print(value); // The output: the 'from the parent'
}, zoneValues: {#key: 'from the parent'});
}
The zone.parent is the parent, and the [#key] reads the parent’s value.
The async propagation
void main() {
runZoned(() {
Future.delayed(Duration.zero, () {
final requestId = Zone.current[#requestId];
print(requestId); // The output: the 'abc-123'
});
}, zoneValues: {#requestId: 'abc-123'});
}
The zone’s values are the ambient in the async callback, and the Zone.current[#requestId] reads the value.
The context table
| The element | The purpose |
|---|---|
zoneValues | The context |
Zone.current[#key] | The read |
zone.parent | The parent |
| The async | The propagation |
| The nesting | The combined |
The framework patterns
| The framework | The use |
|---|---|
| The Flutter | The error reporting |
| The server | The request context |
| The test | The fake async |
| The tracing | The event interception |
Complete Example Session
// ============================================
// PART 1: THE CURRENT ZONE
// ============================================
void main() {
print(Zone.current); // The output: the _RootZone
}
// ============================================
// PART 2: THE RUN ZONED
// ============================================
void main() {
runZoned(() {
print(Zone.current); // The output: the _CustomZone
});
}
// ============================================
// PART 3: THE ZONE VALUES
// ============================================
void main() {
runZoned(() {
final requestId = Zone.current[#requestId];
print(requestId); // The output: the 'abc-123'
}, zoneValues: {
#requestId: 'abc-123',
});
}
// ============================================
// PART 4: THE RUN ZONED GUARDED
// ============================================
void main() {
runZonedGuarded(() {
Future.delayed(Duration.zero, () {
throw StateError('The async error');
});
}, (error, stack) {
print('Caught: $error');
});
}
// ============================================
// PART 5: THE CUSTOM HANDLER
// ============================================
void main() {
final zone = Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stack) {
print('The zone caught: $error');
},
),
);
zone.run(() {
Future.delayed(Duration.zero, () {
throw StateError('The error');
});
});
}
// ============================================
// PART 6: THE PRINT INTERCEPTION
// ============================================
void main() {
final zone = Zone.current.fork(
specification: ZoneSpecification(
print: (self, parent, zone, line) {
parent.print(zone, '[LOG] $line');
},
),
);
zone.run(() {
print('Hello'); // The output: the [LOG] Hello
});
}
// ============================================
// PART 7: THE TIMER INTERCEPTION
// ============================================
void main() {
final zone = Zone.current.fork(
specification: ZoneSpecification(
createTimer: (self, parent, zone, duration, callback) {
print('The timer: $duration');
return parent.createTimer(zone, duration, callback);
},
),
);
zone.run(() {
Timer(Duration(seconds: 1), () {});
});
}
// ============================================
// PART 8: THE REQUEST CONTEXT
// ============================================
void main() {
runZoned(() {
final requestId = Zone.current[#requestId];
handleRequest(requestId);
}, zoneValues: {
#requestId: 'abc-123',
});
}
void handleRequest(String requestId) {
print('The request: $requestId');
}
// ============================================
// PART 9: THE FLUTTER INTEGRATION
// ============================================
void main() {
runZonedGuarded(() {
runApp(MyApp());
}, (error, stack) {
// The report to the crash service.
});
}
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. The Zone.current is the ambient.
// 2. The runZoned runs the body in the new.
// 3. The zoneValues is the context.
// 4. The runZonedGuarded catches the async.
// 5. The handleUncaughtError is the custom.
// 6. The print and the timer are the interceptable.
// 7. The Flutter uses the runZonedGuarded.
// 8. The server uses the request context.
// 9. The test uses the fake async.
// 10. The zone propagates the async.
The ten parts showed the current zone, the runZoned, the zoneValues, the runZonedGuarded, the custom handler, the print interception, the timer interception, the request context, the Flutter integration, and the complete pattern.
Quick Reference
The Zone API
| The element | The purpose |
|---|---|
Zone.current | The current |
runZoned | The run in the new |
runZonedGuarded | The run with the error |
zone.fork | The create the child |
zone.run | The run the body |
zone.parent | The parent |
The ZoneSpecification Handlers
| The handler | The event |
|---|---|
handleUncaughtError | The uncaught |
run | The sync |
registerCallback | The callback |
scheduleMicrotask | The microtask |
createTimer | The timer |
print | The print |
fork | The fork |
The Zone Values
| The element | The purpose |
|---|---|
zoneValues | The context |
Zone.current[#key] | The read |
| The async | The propagation |
| The nesting | The combined |
The Framework Patterns
| The framework | The use |
|---|---|
| The Flutter | The error reporting |
| The server | The request context |
| The test | The fake async |
| The tracing | The event interception |
The Error Handling
| The mechanism | The catches |
|---|---|
The try/catch | The sync |
The runZonedGuarded | The async |
The handleUncaughtError | The custom |
Best Practices
✅ Do This:
// Use the runZonedGuarded for the async errors.
runZonedGuarded(() {
runApp(MyApp());
}, (error, stack) {
// The report.
}); // ✅
// Use the zoneValues for the context.
runZoned(() {
// ...
}, zoneValues: {#requestId: 'abc-123'}); // ✅
// Use the parent to chain the handlers.
final zone = Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stack) {
parent.handleUncaughtError(zone, error, stack);
},
),
); // ✅
// Use the zone for the request context in the server.
runZoned(() {
// The request handling.
}, zoneValues: {#request: request}); // ✅
❌ Don’t Do This:
// Don't rely on the try/catch for the async.
try {
Future.delayed(Duration.zero, () => throw StateError('x'));
} catch (e) {
// The not reached.
} // ❌
// Don't forget the parent handler.
handleUncaughtError: (self, parent, zone, error, stack) {
// The parent not called.
} // ⚠️ The error is swallowed
// Don't use the zone for the hot path.
runZoned(() {
// The 1,000,000 iterations.
}); // ⚠️ The overhead
// Don't nest the zones without the reason.
runZoned(() {
runZoned(() {
// The deep nesting.
});
}); // ⚠️ The confusing
// Don't use the zone for the global state.
Zone.current[#global] = value; // ❌ The zoneValues
Common Pitfalls
| The pitfall | The reason | The fix |
|---|---|---|
| The async error uncaught | No zone | Use the runZonedGuarded |
| The context lost | No zoneValues | Add the values |
| The error swallowed | The parent not called | Call the parent |
| The performance slow | The many handlers | Minimize the zone |
| The nesting confusing | The deep zones | Flatten |
| The Flutter error missed | No runZonedGuarded | Wrap the runApp |
| The test timer real | No fake async | Use the fake_async |
Real-World Examples
1. The Run Zoned
runZoned(() {
print(Zone.current);
});
2. The Zone Values
runZoned(() {
// ...
}, zoneValues: {#requestId: 'abc-123'});
3. The Run Zoned Guarded
runZonedGuarded(() {
// ...
}, (error, stack) {
print(error);
});
4. The Custom Handler
Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stack) {},
),
);
5. The Print Interception
print: (self, parent, zone, line) {
parent.print(zone, '[LOG] $line');
},
6. The Timer Interception
createTimer: (self, parent, zone, duration, callback) {
return parent.createTimer(zone, duration, callback);
},
7. The Request Context
runZoned(() {
// ...
}, zoneValues: {#request: request});
8. The Flutter Integration
runZonedGuarded(() {
runApp(MyApp());
}, (error, stack) {});
9. The Fake Async
fakeAsync((async) {
// The fake timers.
});
10. The Complete Pattern
runZonedGuarded(() {
runApp(MyApp());
}, (error, stack) {});
Visual
The Zone Nesting
┌────────────────────────────────────────────────────────────┐
│ THE ZONE NESTING │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The root zone │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ The runZoned A │ │ │
│ │ │ zoneValues: {#outer: 'A'} │ │ │
│ │ │ ┌────────────────────────────────────┐ │ │ │
│ │ │ │ The runZoned B │ │ │ │
│ │ │ │ zoneValues: {#inner: 'B'} │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ The Zone.current[#outer] = 'A' │ │ │ │
│ │ │ │ The Zone.current[#inner] = 'B' │ │ │ │
│ │ │ └────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The child reads the parent's values. │
└────────────────────────────────────────────────────────────┘
The Async Propagation
┌────────────────────────────────────────────────────────────┐
│ THE ASYNC PROPAGATION │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ runZoned(() { │ │
│ │ final id = Zone.current[#requestId]; │ │
│ │ Future.delayed(Duration.zero, () { │ │
│ │ final id2 = Zone.current[#requestId]; │ │
│ │ // The id2 === the id. │ │
│ │ }); │ │
│ │ }, zoneValues: {#requestId: 'abc-123'}); │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The async callback captures the zone. │
│ The Zone.current is the same. │
└────────────────────────────────────────────────────────────┘
The Error Handling
┌────────────────────────────────────────────────────────────┐
│ THE ERROR HANDLING │
│ │
│ THE SYNC │
│ ┌──────────────────────────────────────────────────┐ │
│ │ try { │ │
│ │ throw StateError('x'); │ │
│ │ } catch (e) { │ │
│ │ // The caught. │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE ASYNC │
│ ┌──────────────────────────────────────────────────┐ │
│ │ runZonedGuarded(() { │ │
│ │ Future.delayed(Duration.zero, () { │ │
│ │ throw StateError('x'); │ │
│ │ }); │ │
│ │ }, (error, stack) { │ │
│ │ // The caught. │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Framework Patterns
┌────────────────────────────────────────────────────────────┐
│ THE FRAMEWORK PATTERNS │
│ │
│ THE FLUTTER │
│ ┌──────────────────────────────────────────────────┐ │
│ │ runZonedGuarded(() => runApp(MyApp()), │ │
│ │ (error, stack) => report(error)); │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE SERVER │
│ ┌──────────────────────────────────────────────────┐ │
│ │ runZoned(() => handle(request), │ │
│ │ zoneValues: {#request: request}); │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE TEST │
│ ┌──────────────────────────────────────────────────┐ │
│ │ fakeAsync((async) { │ │
│ │ // The fake timers. │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| The item | The value |
|---|---|
The Zone | The execution context |
The Zone.current | The current |
The runZoned | The run in the new |
The runZonedGuarded | The run with the error |
The zoneValues | The context |
The ZoneSpecification | The handlers |
The handleUncaughtError | The async error |
The parent | The chain |
| The propagation | The async |
| The framework | The use |
Key takeaways:
- The zone is the execution context that wraps the async callbacks. It is the ambient the
Zone.currentreturns, and the callbacks the code schedules run in the same zone. - The
runZonedruns the body in the new zone. ThezoneValuesis the context, and theZone.current[#key]reads the value. The context propagates through the async. - The
runZonedGuardedcatches the async errors. Thetry/catchdoes not catch the error the async callback throws. The zone’shandleUncaughtErroris the mechanism. - The
ZoneSpecificationhas the handlers. ThehandleUncaughtError, theprint, thecreateTimer, and thescheduleMicrotaskare the interceptable. Theforkcreates the child, and therunruns the body. - The
parentchains the handlers. The custom handler can call the parent’s or the not. Theparent.handleUncaughtErroris the delegation. - The Flutter uses the
runZonedGuarded. TherunAppis the wrapped, and the handler reports the errors to the crash service. - The server uses the zone for the request context. The
zoneValuesholds the request, and theZone.current[#request]reads it in the async. - The test uses the
fake_async. The zone intercepts the timers, and the test advances the clock. Thefake_asyncis the package the zone-based.
Remember: The Dart’s Zone is the execution context for the async. It carries the context the async cannot otherwise carry, catches the errors the try/catch cannot, and intercepts the timers, the microtasks, and the print. The result is the framework that reports the errors, the server that binds the request, and the test that fakes the clock.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!