DartMiddleTechnical

Как устроены zones в Dart и где они используются для error handling?

Zone — контекст выполнения в Dart, охватывающий весь синхронный и асинхронный код; runZonedGuarded используется для глобального перехвата необработанных ошибок (Flutter crashlytics, логирование).

Zones в Dart

Zone — контекст выполнения, охватывающий синхронный код и все асинхронные операции (Future, Stream, Timer), порождённые внутри него. Zone позволяет перехватывать необработанные ошибки, патчить поведение таймеров и print, а также передавать произвольные данные вниз по стеку без явных параметров.

Иерархия зон

При запуске программы Dart создаёт корневую зону (Zone.root). Вызов Zone.current.fork() или runZoned()/runZonedGuarded() создаёт дочернюю зону, которая наследует поведение родителя, если не переопределяет его.

runZonedGuarded — основной инструмент error handling

import 'dart:async';

void main() {
  runZonedGuarded(
    () {
      // Весь код приложения — синхронный и асинхронный
      Future.delayed(Duration(seconds: 1), () {
        throw StateError('async error');
      });
      Timer.periodic(Duration(seconds: 2), (_) {
        throw ArgumentError('timer error');
      });
    },
    (error, stack) {
      // Перехватывает ВСЕ необработанные ошибки внутри зоны
      print('Caught: $error');
      print(stack);
      // Здесь можно: Sentry.captureException, Crashlytics.recordError
    },
  );
}

Flutter: глобальная обработка ошибок

void main() {
  runZonedGuarded(
    () async {
      WidgetsFlutterBinding.ensureInitialized();

      // Ошибки Flutter-виджетов (синхронный rendering)
      FlutterError.onError = (details) {
        FlutterError.presentError(details);
        // FirebaseCrashlytics.instance.recordFlutterFatalError(details);
      };

      // Ошибки платформы (PlatformDispatcher)
      PlatformDispatcher.instance.onError = (error, stack) {
        // FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
        return true;
      };

      runApp(const MyApp());
    },
    (error, stack) {
      // FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
      print('Zone error: $error');
    },
  );
}

Переопределение поведения через ZoneSpecification

final zone = Zone.current.fork(
  specification: ZoneSpecification(
    print: (self, parent, zone, line) {
      parent.print(zone, '[${DateTime.now()}] $line');
    },
    createTimer: (self, parent, zone, duration, callback) {
      print('Timer created for $duration');
      return parent.createTimer(zone, duration, callback);
    },
    handleUncaughtError: (self, parent, zone, error, stack) {
      print('Zone caught: $error');
    },
  ),
);
zone.run(() {
  print('hello');          // [2025-01-01 ...] hello
  Timer(Duration(seconds: 1), () {});
});

Zone values — неявный контекст

final requestIdKey = Object();

void handleRequest(String requestId) {
  runZoned(
    () async {
      await step1();
      await step2(); // zone value доступен без передачи параметра
    },
    zoneValues: {requestIdKey: requestId},
  );
}

Future<void> step2() async {
  final id = Zone.current[requestIdKey];
  print('Request $id: step2');
}

Подводные камни

  • runZonedGuarded не ловит ошибки за пределами зоны. Если Future создан вне зоны и передан в неё — его ошибки не перехватываются.
  • try/catch vs zone. Зона перехватывает только необработанные (uncaught) ошибки. Если Future обрабатывается через .catchError() или try/catch, zone handler не вызывается.
  • Вложенные зоны. Ошибка пробрасывается в ближайшую родительскую зону с handleUncaughtError — двойной обработчик может привести к дублированию репортов.
  • FlutterError.onError и zone — разные механизмы. Flutter-виджеты бросают через FlutterError, а не через зону; нужны оба обработчика.
  • Zone values не изменяемы после создания зоны. zoneValues передаются при fork; изменить их в рантайме нельзя — только создать вложенную зону.
  • Производительность fork. Создание зоны на каждый HTTP-запрос добавляет небольшой оверхед; использовать осознанно в высоконагруженных серверных приложениях.
  • Isolate != Zone. Зоны работают внутри одного isolate; ошибки из другого isolate не перехватываются zone handler'ом текущего.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics

Как устроены zones в Dart и где они используются для error handling? | Talanto