async/await, десахаризированный
async-функция возвращает Promise; await приостанавливает её, оборачивает операнд через PromiseResolve и возобновляет через микрозадачу, когда операнд урегулируется — генератор плюс драйвер. V8 7.2 (2018) убрал лишние тики await для нативных Promise
Коллега настаивает: «await блокирует, пока данные не вернутся — в этом весь смысл». Тогда почему сервер продолжает обрабатывать 5 000 других запросов, пока один из них await’ит медленный запрос к базе? Потому что await ничего не блокирует. Он приостанавливает одну функцию и возвращает поток. Понимание разницы — приостанавливает, а не блокирует — это грань между тем, кто использует async/await, и тем, кто умеет отлаживать его под нагрузкой.
async/await — это генератор плюс драйвер
async/await — это чистый синтаксический сахар; движок десахаризирует его в корутину. async-функция механически — это генератор, чьи точки yield — это await’ы, в паре с автоматическим драйвером, который продвигает генератор каждый раз, когда await’ируемое значение урегулируется. Два факта выпадают сразу:
async-функция всегда возвращает Promise. Её вызов выполняет тело синхронно до первогоawait, затем возвращает pending-Promise, представляющий «итоговое завершение этой функции».return xвнутри — fulfils этот Promise значениемx; брошенная ошибка — rejects его.await x— это точка приостановки. Когда управление доходит доawait x, движок: оборачиваетxчерезPromiseResolve(превращая любое значение или thenable в Promise, на который подписаться), регистрирует продолжение как реакцию на этом Promise и возвращается из функции — отдавая поток назад хосту. Когда await’ируемый Promise урегулируется, зарегистрированная реакция (микрозадача) возобновляет функцию сразу послеawait, подставив значение (или перебросив отклонение).
Поскольку возобновление — это микрозадача, async-функция с двумя await’ами — не одна синхронная единица: она нарезана на куски, которые чередуются с каждой другой ожидающей микрозадачей между возобновлениями. Это линза урока: await — это yield драйверу, а драйвер работает на очереди микрозадач.
История: await раньше стоил три тика
Исходная спецификация ES2017 для await была расточительной. Чтобы обработать случай, когда вы await’ите thenable, спецификация говорила: возьми операнд, создай совершенно новый одноразовый Promise, урегулируй его операндом, затем .then на нём, чтобы подписаться. Даже когда операнд уже был нативным Promise, вы платили за одноразовый Promise и его job принятия thenable. Стоимость, измеренная в тиках микрозадач до возобновления функции, была около трёх тиков для await p, где p — нативный Promise, против одного тика для эквивалентного p.then(...). В горячем цикле, полном await’ов, это утраивало трафик микрозадач.
// До V8 7.2, `await p` был примерно эквивалентен:
const throwaway = new Promise(resolve => resolve(p)); // принять p — лишние тики
throwaway.then(resume); // подписаться — ещё тик
// ≈ 3 тика микрозадач до возобновления, даже для нативного Promise pV8 7.2 (2018): await ≈ p.then
V8 7.2 выпустил оптимизацию await. Когда await’ируемый операнд — нативный Promise (настоящий Promise, а не чужой thenable), движок полностью пропускает танец с одноразовым Promise и регистрирует реакцию-возобновление прямо на операнде — ровно как сделал бы p.then(resume). Это схлопывает await p с ~3 тиков до 1 тика, совпадая с написанным вручную .then. Затем TC39 изменил саму спецификацию под это (флаг --harmony-await-optimization гейтил это во время раскатки; теперь это поведение по умолчанию и спецификации). Практический итог для сеньоров: начиная с движков конца 2018 года, вы больше не платите налог в тик за использование await вместо .then на нативных Promise — пишите то, что читается яснее. Налог возвращается, только если вы await’ите не-нативный thenable, которому всё ещё нужен job принятия.
- await нативного Promise (до 7.2)
- ~3 тика
- await нативного Promise (7.2+, сегодня)
- 1 тик
- p.then(resume) — всегда
- 1 тик
- await не-нативного thenable
- +тик принятия
- Версия V8 с фиксом
- 7.2 (окт 2018)
- Флаг во время раскатки
- --harmony-await-optimization
Zero-cost async stack traces
Та же эпоха релизов принесла zero-cost async stack traces (трассировки стека без накладных расходов во время выполнения). Проблема: когда ошибка всплывает на три await’а вглубь, синхронный стек вызовов давно исчез — каждый await вернулся хосту, поэтому наивная трасса показывает лишь самое внутреннее возобновление без цепочки вызывающих. Старый фикс (оставлен под флагом) записывал стек при каждом создании Promise, что было дорого на горячих путях. Подход zero-cost вместо этого восстанавливает async-фреймы по требованию из цепочки Promise: V8 обходит ссылки [[PromiseFulfillReactions]] (из урока 3) назад, чтобы перестроить логический async-стек вызовов только когда DevTools реально его запрашивает (когда вы попадаете на точку останова или читаете error.stack в контексте отладки). Быстрый путь не платит ничего — никакой аллокации на await — и всё же вы получаете полную async-трассу в отладчике. «Zero-cost» означает нулевую стоимость, когда вы не отлаживаете.
Распространение ошибок: отклонение становится throw
Когда вы оборачиваете await в try/catch и он действительно ловит сетевую ошибку, возникает вопрос: как асинхронное отклонение попадает в синхронный catch-блок? Поскольку реакция-возобновление регистрируется и для fulfilment, и для rejection, await превращает асинхронное отклонение обратно в синхронно выглядящий throw внутри функции. Когда await’ируемый Promise отклоняется, движок возобновляет функцию, бросая причину отклонения в выражении await — поэтому try/catch вокруг await ловит её ровно так, как если бы синхронная функция бросила. В этом весь эргономический выигрыш async/await: линейная обработка ошибок поверх по своей природе нелинейного потока управления. Обратная сторона — ловушка из урока 3: не-await’нутый, не пойманный отклонённый Promise порождает unhandledrejection, потому что не была зарегистрирована реакция, чтобы превратить его в ловимый throw.
Node-сервер await'ит медленный запрос к БД внутри одного обработчика запроса. Что происходит с 5 000 других запросов в полёте во время этого await?
На современном V8 (7.2+), как стоимость в тиках микрозадач `await nativePromise` соотносится с `nativePromise.then(resume)`?
Расставьте по порядку, что делает движок, когда async-функция доходит до `await fetchUser()` и fetch позже fulfils.
- 1 Выполнить тело функции синхронно до выражения await
- 2 Обернуть операнд через PromiseResolve и зарегистрировать на нём продолжение-возобновление
- 3 Вернуть управление хосту, который свободен выполнять другие задачи
- 4 Когда await'ируемый Promise урегулируется, поставить продолжение как микрозадачу
- 5 Микрозадача выполняется: возобновить функцию после await с подставленным значением
▸Граничные случаи
Тонкое следствие «await = приостановка через микрозадачу»: await Promise.resolve() — чистый, идиоматичный способ отложить на один тик — он отдаёт управление очереди микрозадач и возобновляется после текущих ожидающих микрозадач. Но он не отдаёт управление хосту (никакого рендеринга, никакие макрозадачи не выполняются), потому что возобновление — микрозадача, а не макрозадача. Чтобы реально отдать управление рендерингу, нужна граница макрозадачи (setTimeout, MessageChannel, scheduler.yield()) — различие из урока 2, которое решает, сможет ли страница отрисоваться.
- 01Десахаризируйте async/await: что возвращает async-функция и что именно делает await?
- 02Почему await когда-то стоил ~3 тика и что изменил V8 7.2?
- 03Как работают zero-cost async stack traces и что значит «zero-cost»?
async/await — синтаксический сахар, который движок десахаризирует в корутину: async-функция — это генератор, чьи await’ы — это yields, продвигаемый автоматическим драйвером. Её вызов выполняется синхронно до первого await и возвращает pending-Promise (return fulfils его, throw rejects). await x оборачивает x через PromiseResolve, регистрирует двойное продолжение fulfil/reject как реакцию и возвращает управление хосту — приостанавливая функцию, никогда не блокируя поток, поэтому один await’ящий запрос не застопоривает тысячи других. При урегулировании продолжение выполняется как микрозадача, возобновляя функцию после await со значением или перебрасывая отклонение (поэтому try/catch вокруг await работает, давая линейную обработку ошибок поверх нелинейного потока). Исторически await стоил около трёх тиков микрозадач, потому что спецификация принимала операнд через одноразовый Promise; V8 7.2 (октябрь 2018) сделал особый случай для нативных Promise, регистрируя возобновление напрямую, сократив await до одного тика — равно p.then — и TC39 принял это изменение. Та же эпоха принесла zero-cost async stack traces, которые восстанавливают async-фреймы из цепочки Promise по требованию, поэтому быстрый путь ничего не платит. Это закрывает юнит async-deep: от движка без цикла, через две очереди, в конечный автомат Promise и, наконец, в корутину await, построенную поверх него. Теперь, когда встретишь await на горячем пути, умеешь рассуждать о нём: один тик до возобновления для нативного Promise, поток свободен между приостановкой и возобновлением, а try/catch поймает то, что бросил отклонённый Promise — не фольклор, механизм.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в184
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.