Future<T> представляет значение или ошибку, которые придут позже. async помечает функцию, возвращающую Future, а await приостанавливает эту функцию до завершения Future и затем отдаёт значение или бросает его ошибку.
Три состояния: не завершён, завершён значением, завершён ошибкой
**try/catch/finally работают как обычно**: оберните await, и обработка ошибок читается как синхронный код
Поток не блокируется: await возвращает управление event loop, и UI продолжает рисовать кадры, пока вы ждёте
Частая ошибка: try/catch вокруг вызова без await не ловит ничего: ошибка попадает в Future, а не в ваш стек
FlutterJunior
Асинхронность и Future
Future, async/await, then/catchError, FutureBuilder
Закрыто вопросов
0 / 22
- 0Повторить
- 3Часто спрашивают
Вопросы
Нет. await лишь позволяет event loop заняться другой работой, пока ожидается что-то вне вашего изолята: сетевой запрос, чтение файла, таймер. Ваш собственный Dart-код по-прежнему выполняется в том же единственном потоке, между await.
Ждать бесплатно, а работать нет: I/O ожидается вне изолята, а длинный цикл или jsonDecode на несколько мегабайт держат поток, пока не вернутся
**async не дробит работу**: код между двумя await выполняется одним непрерываемым куском
Настоящий параллелизм возможен только через изолят: Isolate.run(...) или флаттеровский compute(...) выполняют работу в другом потоке со своей памятью
Он выведет 1 2 3 4. async-функция выполняется синхронно, на стеке вызывающего, до первого await; там она возвращает незавершённый Future, вызывающий код продолжает работу, а остаток тела выполняется позже.
Вызов не бесплатен: всё до первого await выполняется сразу, поэтому тяжёлая подготовка там блокирует вызывающего
**После await: остаток тела выполняется, когда ожидаемый Future завершится, и никогда не на стеке вызывающего
Готовое значение тоже приостанавливает**: await null или await Future.value(1) всё равно ставят остаток функции в очередь микрозадач
**Ошибки до первого await**: в async-функции они всё равно уходят в возвращённый Future, поэтому try/catch вокруг load() без await ничего не поймает
Оба способа строят один и тот же Future, так что это вопрос читаемости, и async/await обычно выигрывает. .then срабатывает при успехе, .catchError срабатывает при ошибке, а .whenComplete вызывается в обоих случаях, как finally, не видя и не меняя значения.
**await читается сверху вниз**: try/catch/finally выглядят как синхронный код, а зависимые шаги на .then вкладываются друг в друга
Каждый вызов возвращает новый Future: f.catchError(...) не меняет f; использовать нужно тот Future, который он вернул
**whenComplete пропускает результат дальше: значение или ошибка идут без изменений, если только колбэк сам не бросит исключение: тогда эта ошибка заменит исходную
Частая ошибка**: return future; внутри try без await: функция возвращается раньше, чем придёт ошибка, и catch не срабатывает
Timer(duration, callback) вызывает колбэк один раз после задержки, а Timer.periodic вызывает его многократно; оба возвращают Timer, который останавливают через cancel(). Если забыть сделать это в dispose(), колбэк продолжит срабатывать после ухода с экрана.
Что ломается: setState() на удалённом State бросает «setState() called after dispose()», а замыкание держит в памяти State и всё, на что он ссылается
Debounce: на каждое нажатие клавиши отменяйте предыдущий Timer и запускайте новый
Время приблизительное: колбэки идут через очередь событий, и занятый изолят их задерживает; для обратного отсчёта вычисляйте оставшееся время от DateTime, а не считайте тики
FutureBuilder перестраивает часть UI по состоянию Future: ожидание, завершён с данными или завершён с ошибкой. Он не владеет Future, поэтому Future, созданный внутри build(), означает новый запрос на каждый ребилд.
Создайте его один раз: в initState или в поле; заменяйте в didUpdateWidget, когда меняется входной параметр, например id
Почему ребилды частые: setState у родителя, открытие клавиатуры, смена темы; каждый перезапускает запрос и мигает спиннером
Чтение снапшота: сначала проверьте connectionState == ConnectionState.waiting: новый Future сохраняет прежние data и error в снапшоте, пока выполняется; затем hasError, затем hasData
Обновление: присвойте новый Future внутри setState; задайте initialData, если есть закэшированные данные, которые можно показать на это время
Отмены нет: заменённый Future продолжает выполняться; FutureBuilder просто игнорирует его результат
Когда пора переходить дальше: как только нужны повтор, кэш или общие данные между экранами, стейт-менеджер (Cubit, FutureProvider из Riverpod) справляется лучше
Потому что с void вызывающему нечего ожидать: он не может дождаться окончания работы, а ошибки изнутри минуют его try/catch и всплывают как необработанные. Future<void> ничего не стоит и даёт вызывающему и то и другое.
Последовательность: await saveDraft(); close(); работает, только если saveDraft возвращает Future
Тесты: тест не может дождаться async void-метода, поэтому проверяет слишком рано и становится нестабильным
Линты: avoid_void_async подсвечивает такую сигнатуру, а unawaited_futures затем ловит вызовы без await
Исключение: колбэки типа void Function(), например onPressed, принимают async-замыкание, но его никто не ждёт, поэтому ошибки нужно обрабатывать внутри
Функция идёт дальше, не дождавшись окончания работы, а у ошибки этого Future нет слушателя, поэтому она становится необработанной асинхронной ошибкой и не доходит до вашего try/catch. Fire-and-forget допустим только для работы, чей результат никому не нужен, и со своим обработчиком ошибок.
Ошибки порядка: следующая строка выполняется до окончания сохранения, и следующий экран читает устаревшие данные
Потерянные ошибки: они уходят в PlatformDispatcher.onError или в текущую зону, но никогда не к вызывающему
Линт: unawaited_futures подсвечивает Future, который не ожидается и не возвращается; unawaited(f) из dart:async говорит «так задумано»
Подходящие случаи: аналитика, логирование, предзагрузка кэша: unawaited(track(e).catchError(logDrop)) или .ignore(), когда не важна даже ошибка
Никогда: сохранение, платёж или всё, от чего зависят пользователь или следующий экран
forEach игнорирует то, что возвращает колбэк, поэтому Future async-замыкания теряются: syncPhotos показывает тост и завершается раньше, чем закончится хоть одна загрузка, а упавшая загрузка становится неперехваченной ошибкой вместо того, чтобы дойти до вызывающего.
Почему так происходит: forEach принимает void Function(T); async-замыкание подходит под этот тип, но его Future никуда не уходит, поэтому никто его не ждёт и ни один try/catch не видит его ошибку
Второй эффект: все загрузки стартуют разом, без ограничения, а вызовы markSynced завершаются в произвольном порядке
Исправление: цикл for-in внутри async-функции ожидает каждую загрузку по очереди, поэтому тост показывается после последней, а первая же ошибка доходит до вызывающего; await Future.forEach(photos, ...) делает то же самое
Если параллельность нужна: await Future.wait(photos.map(uploadAndMark)) сохраняет параллельность, но всё равно ждёт и всё равно сообщает об ошибках; для длинных списков ограничьте число одновременных запросов
Как поймать в следующий раз: линт avoid_function_literals_in_foreach_calls из набора recommended помечает forEach с функциональным литералом; тест, который проверяет загрузки после await syncPhotos(...), падает сразу
Сначала запустите все Future, а затем ждите их вместе: Future.wait([...]) или, в Dart 3, через запись (record), например await (fetchUser(), fetchPrefs()).wait. Если ждать их по одному, общее время станет суммой вызовов, а не временем самого долгого.
**Future.wait**: результаты приходят в исходном порядке одним List<T>; разные типы схлопываются в общий супертип, обычно List<Object>, и требуют приведений
**.wait у записи: каждая позиция сохраняет свой тип и сразу деструктурируется в переменные
При ошибке**: Future.wait всё равно дожидается всех, затем бросает первую ошибку и теряет успешные результаты; eagerError: true бросает при первой ошибке. Отмены не происходит ни в каком случае
Сохранить частичные результаты: .wait у записи и у списка бросает ParallelWaitError, где есть и values, и errors; либо дайте каждому Future свой фолбэк через .catchError
**Future.any: завершается первым завершившимся Future, и эту гонку может выиграть ошибка
Следите за количеством**: сотни запросов разом могут упереться в rate limit; ограничьте их небольшим пулом
Каждый изолят работает в одном потоке с event loop и двумя очередями. Цикл берёт одно событие, выполняет его до конца, затем полностью опустошает очередь микрозадач и только потом берёт следующее событие, поэтому микрозадачи всегда идут первыми.
Очередь событий: таймеры, Future(...), Future.delayed, завершённый I/O, сообщения платформы и события касаний; колбэки кадров Flutter выполняются в том же потоке между ними
Очередь микрозадач: scheduleMicrotask, Future.microtask, колбэки на уже завершённом Future, включая await на готовом значении
Выполнение до конца: работающий Dart-код ничто не прерывает; длинный синхронный кусок задерживает все события, включая следующий кадр
Голодание: микрозадача, которая всё время планирует новые микрозадачи, не даёт очереди событий выполниться: ни ввода, ни таймеров, ни кадров
Уступить управление: await Future<void>.delayed(Duration.zero) ставит остаток функции за ожидающими событиями, и длинный цикл даёт UI отработать
Параллелизма нет: цикл чередует задачи; чтобы код выполнялся одновременно, нужен другой изолят
A F G D E B C. Сначала выполняется синхронный код (Future.sync вызывает свою функцию сразу), затем вся очередь микрозадач (D, E), затем очередь событий по порядку (B, C).
**Future(...)**: ставит функцию в очередь событий, как Timer с нулевой задержкой
**Future.delayed(Duration.zero, ...): тоже событие таймера; таймеры с одинаковой задержкой срабатывают в порядке создания
scheduleMicrotask / Future.microtask: очередь микрозадач, она выполняется раньше любого события
Future.value(x)**: завершается в микрозадаче, поэтому его .then выполняется сразу после D
**Future.sync(f)**: выполняет f сейчас, на стеке вызывающего, и лишь оборачивает результат или брошенную ошибку в Future
Что проверяет интервьюер: что колбэк .then никогда не выполняется синхронно, даже на уже завершённом Future
Completer<T> создаёт Future, который вы завершаете вручную: отдаёте наружу completer.future, а когда придёт результат, вызываете complete(value) или completeError(error, stackTrace). Его используют, чтобы превратить API на колбэках во Future.
Типичные случаи: обёртка над слушателем или SDK на колбэках, ожидание разового события вроде первого кадра или ответа платформы
Завершить ровно один раз: повторный complete бросает StateError; проверяйте isCompleted, если колбэки могут соревноваться
Каждая ветка должна завершать: ветка ошибки или снятый слушатель, которые его не завершают, оставляют вызывающих ждать вечно; добавьте .timeout, если источник может замолчать
Передавайте стек-трейс: completeError(e, st), иначе трейс начнётся в Completer, а не в месте реального сбоя
Не оборачивайте им готовый Future: async/await короче и не может забыть завершиться
Тип FutureOr<T> означает «либо T, либо Future<T>». Функция возвращает его, когда результат то готов сразу, то нет (например, попадание в кэш против запроса в сеть), и быстрый путь обходится без прохода через event loop.
**Его можно await: ожидать обычное значение разрешено, это стоит одну микрозадачу
Где он встречается чаще всего**: параметры, принимающие обе формы: колбэк .then, Completer.complete, задача FutureOr<T> Function(), переданная во вспомогательную функцию
**async-функция никогда не идёт быстрым путём: она всегда возвращает Future, даже для уже готового значения
В своём API используйте осторожно**: вызывающим приходится обрабатывать обе формы, а проверки is Future становятся хрупкими; возвращайте Future<T>, если профилирование не показало, что синхронный путь важен
load не помечена async, поэтому throw происходит на стеке вызывающего ещё до того, как появился какой-либо Future. Исключение вылетает из самого выражения load(id), и .catchError даже не успевает подписаться.
**.catchError видит только ошибки, которые несёт Future: синхронный throw никогда им не становится
Вариант 1**: пометить load как async; тогда любой throw в её теле завершает возвращаемый Future этой ошибкой
Вариант 2: оставить функцию без async, но обернуть тело в Future.sync(...), который превращает синхронный throw в упавший Future
**Future.value(parse(raw)) не поможет**: аргумент вычисляется и бросает исключение ещё до вызова Future.value
Правило: функция, возвращающая Future, не должна бросать синхронно; async даёт самый простой способ это гарантировать
return api.fetchProfile(id) возвращает Future без await, поэтому выход из блока try происходит раньше, чем запрос упадёт. Ошибка приходит позже, уже вне try: ветка on NetworkException не выполняется, и вызывающий получает исключение вместо кэша.
Почему так происходит: в async-функции return future сразу выходит из try; собственный Future функции затем принимает результат возвращённого, включая ошибку, и обработчика вокруг уже нет
Исправление: return await внутри try дожидается результата там же, поэтому ошибка выбрасывается, пока catch ещё активен
**Вне try разницы нет**: поэтому линт unnecessary_await_in_return помечает такой await в других местах, но не трогает его внутри try
**Та же ловушка с finally: он выполняется до завершения возвращённого Future, поэтому очистка вроде скрытия спиннера или закрытия клиента происходит слишком рано
Как поймать в следующий раз**: стандартного линта на эту строку нет (unnecessary_await_in_return намеренно не трогает await внутри try), поэтому полагайтесь на юнит-тест с фейковым API, бросающим NetworkException, и правило ревью: внутри try всегда return await
future.timeout(duration) возвращает новый Future, который падает с TimeoutException, если исходный выполняется слишком долго, или возвращает фолбэк из onTimeout. Повторы пишут сами или берут из пакета вроде retry: цикл с растущей паузой и ограничением.
Таймаут ничего не отменяет: исходный запрос продолжает выполняться, вы просто перестаёте его ждать; чтобы остановить работу, отменяйте у источника (CancelToken в Dio, http.Client.close())
Экспоненциальная задержка с джиттером: 0,5 с, 1 с, 2 с плюс немного случайности, чтобы тысячи клиентов не повторяли запросы синхронно
Повторяйте только то, что может пройти позже: таймауты, разорванные соединения, 5xx, 429 (с учётом Retry-After); никогда 400, 401 или 404
Ограничьте число попыток: три–пять, затем отдайте ошибку наверх; бесконечные повторы превращают сбой в лавину, которую вы создали сами
Идемпотентность: повтор платежа или «создать заказ» может выполнить его дважды, если сервер не принимает ключ идемпотентности
Установите в main() два хука: FlutterError.onError для ошибок, которые фреймворк ловит в build, layout, paint и жестах, и PlatformDispatcher.instance.onError для необработанных асинхронных ошибок. try/catch вокруг runApp() видит только синхронные исключения самого вызова.
Почему try/catch не работает: runApp возвращается почти сразу; дальнейшие ошибки возникают в кадрах, колбэках и Future уже после выхода из блока try
**FlutterError.onError**: вызывайте FlutterError.presentError(details), чтобы сохранить вывод в консоль, затем передавайте ошибку в крэш-репортер
**PlatformDispatcher.instance.onError**: верните true, чтобы пометить ошибку обработанной; этот способ заменил runZonedGuarded как рекомендуемый
**runZonedGuarded**: по-прежнему работает, но ensureInitialized и runApp должны выполняться в одной зоне, иначе Flutter предупредит о «Zone mismatch»
**ErrorWidget.builder: заменяет стандартный экран ошибки (красный в debug, серый в release) спокойной заглушкой
Не покрывается**: ошибкам в других изолятах нужен Isolate.addErrorListener или порт onError у Isolate.spawn
Отменяют то, что выполняет работу, а не Future: Future служит лишь дескриптором результата. Если источник остановить нельзя, дайте ему завершиться и проигнорируйте результат, когда он придёт.
Отмена у источника: CancelToken в Dio, http.Client.close(), StreamSubscription.cancel(), Timer.cancel(), сообщение или kill() рабочему изоляту
Игнорировать устаревшие ответы: проверяйте mounted после await или держите счётчик запросов и отбрасывайте ответы, которые уже не последние, как в поиске по мере ввода
**CancelableOperation**: из package:async; cancel() вызывает ваш onCancel, где вы останавливаете источник, и после этого его value уже не завершится
Привяжите к жизненному циклу: dispose(), close() у Bloc, ref.onDispose в Riverpod
Частая ошибка: считать, что работа остановилась вместе с экраном; она продолжает тратить сеть, батарею и память и может ещё записать в состояние
Future сообщает об ошибке как о необработанной в момент завершения, если к нему ещё не подключён слушатель. Ваш await секундой позже подписывается на Future, который уже отправил ошибку в PlatformDispatcher.onError, поэтому вы видите её и в своём catch, и в крэш-логе.
Правило: обработчик должен быть подключён до того, как Future может завершиться, а не тогда, когда вы дойдёте до await
Исправление: _config = loadConfig()..ignore(); сразу помечает ошибку обработанной; последующий await _config всё равно бросит её в ваш try/catch
Или подключите настоящий обработчик: loadConfig().catchError(...) с фолбэк-значением, если есть разумное значение по умолчанию
Где встречается: Future, запущенный в конструкторе или initState и ожидаемый позже в обработчике; кэши, которые хранят Future для повторного использования
Почему плавает: быстрая ошибка завершается до await, а медленная завершается после, поэтому баг воспроизводится только в некоторых сетях
Любое место, где ошибку пробрасывают дальше или оборачивают без исходного StackTrace. VM и так сшивает асинхронные фреймы через await, поэтому трейс обычно рвётся в ваших собственных блоках catch, колбэках catchError и Completer.
**rethrow, а не throw e**: throw e начинает новый трейс в блоке catch
Обёртка: throw SaveFailed(e) указывает на обёртку; Error.throwWithStackTrace(SaveFailed(e), st) сохраняет исходный трейс
Обработчики с двумя аргументами: catch (e, st) и .catchError((Object e, StackTrace st) {...}); форма с одним аргументом трейса не видит, поэтому всё, что она оборачивает или пробрасывает, остаётся без него
Completer: completeError(e) без st записывает трейс там, где вы завершили Completer, а не там, где произошёл сбой
Репортеры: передавайте полученный st, а не StackTrace.current, прочитанный внутри catch
Обфусцированные релизные сборки: с --obfuscate --split-debug-info трейсы нечитаемы, пока вы не загрузите файлы символов в крэш-репортер
Каждый await создаёт паузу, в которую выполняется другой код, поэтому два вызова вида «прочитать, подождать, записать» могут перемешаться даже в одном потоке. Решение: заставить второго вызывающего дождаться работы первого или разделить её, а не запускать свою.
Single-flight: храните Future текущего запроса; следующие вызывающие получают тот же Future, а поле очищается по завершении. Классическим случаем служит обновление токена после пачки ответов 401
Lock: цепляйте каждую операцию к предыдущей, чтобы они шли строго по одной; Lock из package:synchronized делает именно это
Очередь: один потребитель, обрабатывающий события по порядку, например stream.asyncMap(handle), для записей, которые должны сохранять порядок
Опустите на уровень ниже: пусть хранилище делает это атомарно: SQL-транзакция или UPDATE ... WHERE rev = ?
Частая ошибка: флаг bool _busy, который проверяют до await, а выставляют после; оба вызывающих проходят проверку
Между изолятами: ничего из этого не помогает; изоляты не делят память, поэтому направляйте работу через один изолят-владелец или через базу данных