Перейти к содержимому

Правила ошибок

Страница «Ошибки» прослеживает ошибку от задачи до того, кто за неё отвечает. Эта страница даёт правила этого маршрута: через какие места проходит ошибка каждого вида, отладочный канал, который это показывает, ошибку, в которой несколько провалов, и редкие случаи. Код продолжает ту страницу: миграция из раздела «Куда уходят ошибки», которую останавливает токен, пока пользователь отменяет, фильтр из раздела «Ошибки отмены» и наблюдатель DatabaseErrors из раздела «Ответ за ошибки».

Строки под кодом — то, что код печатает:

Строка Что это
cancel пользователь отменяет задачу
outcome: то, чем завершается job.done
onUnanswered: ошибка, за которую наблюдатель ответил
zone: ошибка, дошедшая до зоны неперехваченной

Ошибка проходит места из вступления страницы «Ошибки» по порядку: onError, исход, фильтр Job.isCancellationError, ответ и зону. Таблица показывает, какие из них проходит каждая ошибка, с наблюдателем и без него. Ответ — это onUnanswered наблюдателя, который подмешивает JobAnswerer; у задачи без такого наблюдателя ответ — это зона. Более редкие маршруты, например маршруты ctx.runAll и движков, собраны в разделе «Редкие маршруты» ниже.

Ошибка С наблюдателем Без него
Ошибка тела, и задача кончается с ней Failed onError, затем ответ, если исход никто не наблюдал Зона, если исход никто не наблюдал
Ошибка тела, и задача кончается Cancelled onError, затем ответ Зона
Поздняя ошибка действия, ожидание которого прервал ctx.abandonable onError, затем ответ Зона
Ошибка уборки onError, затем ответ Зона
Ошибка колбэка ctx.onCancel, job.whenCancelled или job.whenFinished onError, затем ответ Зона
Ошибка работы ctx.unattended onError, затем ответ Зона

Что не пускает ошибку к ответу.

  • ignoreFailure() у задачи останавливает провал её тела на onError. Этот вызов отпускает провалы только этой задачи: дочерняя задача, чьё тело упало, отвечает за свой провал сама, что бы ни вызвал её родитель.
  • Job.isCancellationError, когда задача приняла отмену, останавливает названную им ошибку на onError, откуда бы ошибка ни пришла: из тела, из шага ctx.uncancellable, из дочерней задачи, которую задача ждала, из уборки. Это касается строк, которые кончаются ответом, в этой таблице и в разделе «Редкие маршруты»: строк тела отменённой задачи и строк ошибок вне тела.
  • Исход забирает только тот провал, который несёт. Когда задача кончается Cancelled, её исход несёт отмену, поэтому провал её тела уходит в onUnanswered или в зону, даже если кто-то читает done или value, и так же уходит провал ветки ctx.runAll, чья группа бросила другую ошибку.

Одна ошибка, несколько вызовов.

  • Приложение, которое сообщает об ошибках и в onError, и в зоне, слышит дважды ошибку, дошедшую до обоих. Таблица показывает один маршрут от задачи в зону — ответ, когда за ошибку никто не отвечает или onUnanswered отправляет её дальше или бросает. Движок, который нарочно кончает задачу Failed, добавляет второй, мимо ответа: две последние строки «Редких маршрутов».
  • Один провал, ставший исходом нескольких задач, доходит до onError по разу на каждой из них. Ребёнок ctx.run падает, родитель выпускает провал из тела, и наблюдатель, которого ребёнок унаследовал, слышит тот же объект на ребёнке, а потом на родителе. Так же ведут себя ветка ctx.runAll и её родитель, источник и его продолжение then с одним наблюдателем.
  • Наблюдатель, который считает провалы, отличает второй вызов от новой ошибки по identical, а Expando помнит услышанные ошибки и не держит их в памяти. Ошибка, брошенная константой, например throw const SignedOut(), — один и тот же объект при каждом броске, и два таких провала считаются за один. Expando не берёт строку, число, логическое значение и запись и бросает, если их дать: у такого значения нет своей идентичности, по которой отличить два вызова, и наблюдатель считает его при каждом вызове.

Что слышит onError. Параметр answer у onError говорит, что задача делает с ошибкой, и решён до вызова.

answer Когда
answered Ошибка, которой не несёт ни один исход, которая не отмена и которую фильтр не называет: ошибка уборки, колбэка, работы ctx.unattended, поздняя ошибка ctx.abandonable, ошибка имени ребёнка, Failed, который движок передал уже кончившейся задаче, если у неё не вызвали ignoreFailure(). Ответ приходит сразу после onError. А ещё провал тела задачи, которая приняла отмену к тому моменту, когда тело бросило, если фильтр его не называет: ответ придёт, когда задача кончится, если до тех пор не вызовут ignoreFailure(). Переполнение стека, которое тело бросило до того, как вернулся его старт, вместо этого — later: если оно оборвало и старт, ядро кончает задачу, не отвечая за её провал
cancellation Cancelled и ошибка, которую фильтр называет у задачи, принявшей отмену. Failed, который движок собрал вокруг Cancelled, всё равно уходит в зону мимо ответа, если исход никто не наблюдает
never Провал тела или исхода задачи, у которой вызвали ignoreFailure(), Failed, который движок передал такой задаче, когда она уже кончилась, провал источника, который продолжение then или ручка shared несёт после того, как за источник уже ответили, и провал внутри ParallelWaitError, которого не встретил обход, по которому задача решала. Провал, на который не обращают внимания, фильтру не показывают, поэтому never может нести ошибку, которую фильтр назвал бы отменой
later Любой другой провал тела или исхода: исход ещё могут прочитать, и ответ, если будет, придёт, когда задача кончится

later ничего не обещает, и три его ошибки могут кончиться без ответа, а наблюдателю больше ничего не скажут. Первая — провал тела задачи, которая приняла отмену только после того, как тело бросило. Вторая — провал источника, когда onError продолжения отменяет это продолжение. Обе покрывает отмена, пришедшая после onError, фильтр спрашивают о них тогда, и ошибка, которую он назвал, ответа не получает. Третья — переполнение стека, которое тело бросило до того, как вернулся его старт, если оно оборвало и старт: ядро кончает задачу, не отвечая за её провал. ParallelWaitError, второй обход которого встречает не те провалы, что первый, слышен так, как его встречает второй обход, а за провал, который встретил только первый, отвечают, хотя onError его не слышал. Приложение, которое сообщает об ошибках и в onError, и в ответе, оставляет вызовы с answered ответу и всё равно слышит дважды провал тела, исход которого никто не наблюдает.

При отладке ошибки, которые задача отдаёт своему наблюдателю, видны, даже если наблюдателя не писать. Приложение включает отладочный канал в main:

Job.debug = print;

Канал печатает запуск, отмену и конец каждой задачи. Ещё он печатает каждую ошибку, которую задача отдаёт наблюдателю, есть у задачи наблюдатель или нет, а если ошибка ушла дальше в зону, говорит и об этом. Чтение done или value у задачи Job.deferred, которую никто не запустил, тоже получает строку, Job() awaited before start: ожидание такой задачи не кончится, пока её не запустят или не отменят, а задача, которую никто не запускает, больше ничего не печатает. Первая попытка из раздела «Куда уходят ошибки» под каналом:

Job() started
cancel
cancel Job(): Cancelled(manual)
Job() error: DatabaseStopped
Job() finished: Cancelled(manual)
Job() error nobody answered for: DatabaseStopped
Job() error went to the zone: DatabaseStopped
zone: DatabaseStopped
outcome: Cancelled(manual)

Строки, которые начинаются с Job(), и строка cancel Job() — строки канала. С фильтром вместо трёх строк после Job() finished канал печатает одну: Job() kept a cancellation error out of the answer: DatabaseStopped. Фильтр один на всё приложение, и по каналу видно, почему ошибка так и не дошла до зоны.

ParallelWaitError занимает в канале одну строку, целиком, тогда как onError слышит каждый провал внутри него по отдельности. Канал один на все задачи приложения и говорит с тем, кто читает консоль. Через наблюдателя приложение слышит то, на что ему надо отреагировать.

Задача отдаёт ctx.unattended две операции, которых ждут вместе. saveDraft падает с DatabaseException, а sendAnalytics падает со StateError. [saveDraft(), sendAnalytics()].wait бросает ParallelWaitError, в котором лежат оба провала:

final job = Job<void>(observer: DatabaseErrors(), (ctx) async {
ctx.unattended(() => [saveDraft(), sendAnalytics()].wait);
});
outcome: Done(null)
onUnanswered: DatabaseException
zone: Bad state: analytics offline

onUnanswered спрашивают об одном провале за раз. Задача обходит ParallelWaitError и спрашивает о каждом провале внутри него, со стектрейсом этого провала. Поэтому за ошибку базы данных отвечает наблюдатель, а ошибку аналитики зона слышит отдельно. За Failed с такой ошибкой, который никто не наблюдает, отвечают так же, провал за провалом. О неперехваченном Cancelled, одном или внутри ParallelWaitError, никого не спрашивают: отмена не провал. Не спрашивают никого и об ошибке, которую называет Job.isCancellationError, когда задача приняла отмену.

onError тоже слышит их по одной, а Cancelled внутри ParallelWaitError слышит отдельным вызовом. Исход хранит ошибку такой, какой её бросили: тело, упавшее с ParallelWaitError, кончается Failed с этой ошибкой. Job.visitErrors делает тот же обход для того, кто читает исход:

final outcome = await job.done;
if (outcome case Failed(:final error, :final stackTrace)) {
Job.visitErrors(
error,
stackTrace,
onFailure: (failure, failureStackTrace) => print('failed: $failure'),
);
}

Job.visitErrors отдаёт Cancelled в onCancelled, а любую другую ошибку — в onFailure. ParallelWaitError он обходит в глубину, ветки по порядку, каждую со своим стектрейсом, и пропускает ветку null: она завершилась успешно. Ничего не теряется: то, что обход прочесть не может, уходит в onFailure целиком, со стектрейсом объемлющей ошибки, как и ошибка, в которой нет ни провала, ни отмены. Каждый ParallelWaitError обходится один раз и целиком отдаётся не больше одного раза, сколько бы веток к нему ни вело.

Здесь пределы фильтра и более редкие маршруты ошибки, в том числе ошибки, которые Dart отдаёт зоне мимо задачи.

Ошибка, которую тело делает из собственной отмены, до фильтра тоже доходит, но проверка типа из раздела «Ошибки отмены» её не узнаёт. Тело, которое ловит всё вокруг ctx.abandonable и бросает вместо пойманного свою ошибку, например throw AppException(error), заворачивает каждый Cancelled в AppException, и зона слышит его при каждой отмене. Фильтр называет обёртку по причине:

Job.isCancellationError = (error) =>
error is AppException && error.cause is Cancelled;

Вместо этого catch может первым делом вызвать ctx.check(), как в разделе «Спросить задачу» страницы об отмене: тогда наружу уходит сама отмена, а отмена не провал. Это обходится дороже фильтра: когда задача приняла отмену, ctx.check() бросает её, что бы ни поймал catch. Тело, которое ждёт через ctx.join, так теряет настоящий провал, дошедший до catch после отмены: например, открытие, которое нашло базу заблокированной. Его не слышат ни onError, ни зона. Фильтр не пускает только те ошибки, которые называет.

Фильтр только не пускает ошибки к ответу, и ctx.runAll это показывает. Ветка, которую отменили снаружи и чьё тело бросило ошибку отмены, для группы упала. Поэтому группа отменяет остальные ветки с SiblingCancelReason, у которой причиной стоит эта ошибка. Ветка, которую никто не отменял и которая кончилась Failed с такой ошибкой, считается обычным провалом. Группа бросает её ошибку вместо отмены другой ветки, и тело родителя получает эту ошибку, а не Cancelled.

Здесь маршруты ветки ctx.runAll, имени дочерней задачи, Cancelled и движка.

Ошибка С наблюдателем Без него
Ошибка тела в ветке ctx.runAll, чья группа бросает другой провал onError, затем ответ Зона
Ошибка toString ключа дочерней задачи, её describe или её Cancelled, когда ядро называет её в отмене родителя onError, затем ответ Зона
Cancelled, брошенный вне тела onError Никто
Собственная отмена задачи из действия, ожидание которого прервал ctx.abandonable, или из работы ctx.unattended Никто Никто
Провал задачи, которую движок заканчивает Failed с Cancelled, чтобы провалить её нарочно onError, затем зона мимо ответа, если исход никто не наблюдал Зона, если исход никто не наблюдал
Провал задачи, которую движок заканчивает Failed с ParallelWaitError из одних отмен onError, затем зона мимо ответа, если исход никто не наблюдал Зона, если исход никто не наблюдал

Что Dart отдаёт зоне мимо задачи. Вызов контекста, которого тело не ждёт, бросает в future, которую никто не ждёт. Эту ошибку в зону, где идёт тело, отдаёт не задача, а Dart.

  • Ошибка такого вызова уходит туда с наблюдателем и без него. У задачи, созданной через Job.deferred, зона, где идёт тело, — это зона, откуда её запустили.
  • Пока идёт тело, отмена задачи заставляет ctx.abandonable, вызванный без await, бросить Cancelled в свою future, и Dart отдаёт собственную отмену задачи этой зоне, как любую другую ошибку.
  • Когда тело кончилось, исключение одно — ctx.abandonable: действие, которое тогда падает, идёт маршрутом поздней ошибки из раздела «Куда идёт каждая ошибка», к onError и дальше к ответу. Вызов ctx.abandonable без await доходит до зоны, где идёт тело, со своей ошибкой или с отменой задачи, только пока тело идёт.

Фильтр не видит ни одной ошибки, которую Dart отдаёт зоне, и ошибка async-колбэка ctx.onCancel — одна из них, как показывает раздел «Остановка, которая занимает время».