Правила ошибок
Страница «Ошибки» прослеживает ошибку от задачи до того, кто
за неё отвечает. Эта страница даёт правила этого маршрута: через какие места
проходит ошибка каждого вида, отладочный канал, который это показывает, ошибку,
в которой несколько провалов, и редкие случаи. Код продолжает ту страницу:
миграция из раздела «Куда уходят ошибки»,
которую останавливает токен, пока пользователь отменяет, фильтр из раздела
«Ошибки отмены» и наблюдатель 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() startedcancelcancel Job(): Cancelled(manual)Job() error: DatabaseStoppedJob() finished: Cancelled(manual)Job() error nobody answered for: DatabaseStoppedJob() error went to the zone: DatabaseStoppedzone: DatabaseStoppedoutcome: 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: DatabaseExceptionzone: Bad state: analytics offlineonUnanswered спрашивают об одном провале за раз. Задача обходит
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 — одна из них, как показывает раздел
«Остановка, которая занимает время».