| |

Dart 63 🎯 Zone Execution Contexts (Zone): Custom Error Zones and Context Binding

⏱️ Estimated read: 12 min | |

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 handlerThe event
handleUncaughtErrorThe uncaught error
runThe sync body
runUnaryThe unary callback
runBinaryThe binary callback
registerCallbackThe callback registration
registerUnaryCallbackThe unary
registerBinaryCallbackThe binary
errorCallbackThe error callback
scheduleMicrotaskThe microtask
createTimerThe timer
createPeriodicTimerThe periodic
printThe print
forkThe 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 elementThe purpose
Zone.currentThe current zone
runZonedThe run in the new
runZonedGuardedThe run with the error
zone.forkThe create the child
zone.runThe run the body
zoneValuesThe context
ZoneSpecificationThe 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 mechanismThe catches
The try/catchThe sync
The runZonedGuardedThe async
The handleUncaughtErrorThe custom
The FlutterError.onErrorThe Flutter
The PlatformDispatcher.onErrorThe 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 elementThe purpose
zoneValuesThe context
Zone.current[#key]The read
zone.parentThe parent
The asyncThe propagation
The nestingThe combined

The framework patterns

The frameworkThe use
The FlutterThe error reporting
The serverThe request context
The testThe fake async
The tracingThe 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 elementThe purpose
Zone.currentThe current
runZonedThe run in the new
runZonedGuardedThe run with the error
zone.forkThe create the child
zone.runThe run the body
zone.parentThe parent

The ZoneSpecification Handlers

The handlerThe event
handleUncaughtErrorThe uncaught
runThe sync
registerCallbackThe callback
scheduleMicrotaskThe microtask
createTimerThe timer
printThe print
forkThe fork

The Zone Values

The elementThe purpose
zoneValuesThe context
Zone.current[#key]The read
The asyncThe propagation
The nestingThe combined

The Framework Patterns

The frameworkThe use
The FlutterThe error reporting
The serverThe request context
The testThe fake async
The tracingThe event interception

The Error Handling

The mechanismThe catches
The try/catchThe sync
The runZonedGuardedThe async
The handleUncaughtErrorThe 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 pitfallThe reasonThe fix
The async error uncaughtNo zoneUse the runZonedGuarded
The context lostNo zoneValuesAdd the values
The error swallowedThe parent not calledCall the parent
The performance slowThe many handlersMinimize the zone
The nesting confusingThe deep zonesFlatten
The Flutter error missedNo runZonedGuardedWrap the runApp
The test timer realNo fake asyncUse 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 itemThe value
The ZoneThe execution context
The Zone.currentThe current
The runZonedThe run in the new
The runZonedGuardedThe run with the error
The zoneValuesThe context
The ZoneSpecificationThe handlers
The handleUncaughtErrorThe async error
The parentThe chain
The propagationThe async
The frameworkThe use

Key takeaways:

  • The zone is the execution context that wraps the async callbacks. It is the ambient the Zone.current returns, and the callbacks the code schedules run in the same zone.
  • The runZoned runs the body in the new zone. The zoneValues is the context, and the Zone.current[#key] reads the value. The context propagates through the async.
  • The runZonedGuarded catches the async errors. The try/catch does not catch the error the async callback throws. The zone’s handleUncaughtError is the mechanism.
  • The ZoneSpecification has the handlers. The handleUncaughtError, the print, the createTimer, and the scheduleMicrotask are the interceptable. The fork creates the child, and the run runs the body.
  • The parent chains the handlers. The custom handler can call the parent’s or the not. The parent.handleUncaughtError is the delegation.
  • The Flutter uses the runZonedGuarded. The runApp is the wrapped, and the handler reports the errors to the crash service.
  • The server uses the zone for the request context. The zoneValues holds the request, and the Zone.current[#request] reads it in the async.
  • The test uses the fake_async. The zone intercepts the timers, and the test advances the clock. The fake_async is 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.


Was this helpful?


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!