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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.