Асинхронный JavaScript
Колбэки, промисы и их комбинаторы, async/await, обработка ошибок и observable.
12 вопросов
JuniorТеорияОчень частоЧто делают async и await и как они связаны с промисами?
Что делают async и await и как они связаны с промисами?
async/await — это синтаксический сахар над промисами. async-функция всегда возвращает промис. await приостанавливает функцию, пока ожидаемый промис не разрешится, затем возобновляет её со значением успеха или выбрасывает причину отклонения. Это позволяет писать async-код, читающийся сверху вниз как синхронный, без цепочек .then().
Типичные ошибки
- ✗Думать, что
awaitблокирует поток, а не приостанавливает только async-функцию - ✗Считать, что
async-функция возвращает обычное значение, а не промис - ✗Полагать, что код после
awaitвыполняется синхронно, а не как продолжение
Уточняющие вопросы
- →Какое значение возвращает
async-функция, если её тело возвращает не-промис? - →Как
awaitпроявляет отклонённый промис внутри тела функции?
JuniorТеорияОчень частоЧто такое промис и каковы его три состояния?
Что такое промис и каковы его три состояния?
Промис — это объект, представляющий будущий результат асинхронной операции. Он pending, пока не разрешится, затем либо fulfilled (со значением), либо rejected (с причиной). Он разрешается один раз и далее неизменяем. .then() регистрирует обработчик успеха, .catch() — обработчик ошибки, а .finally() выполняется в любом случае.
Типичные ошибки
- ✗Думать, что разрешённый промис может снова сменить состояние — разрешение одностороннее и финальное
- ✗Считать, что
.then()выполняется синхронно, а не планирует микрозадачу - ✗Путать
rejectedс выбросом — отклонение это сохранённая причина, а не выброшенный стек
Уточняющие вопросы
- →Что получает функция-исполнитель, переданная в
new Promise(executor)? - →Почему колбэк
.then()всегда выполняется асинхронно, даже если промис уже разрешён?
JuniorТеорияЧастоЧто такое функция-колбэк и что такое callback hell?
Что такое функция-колбэк и что такое callback hell?
Колбэк — это функция, переданная аргументом, чтобы быть вызванной позже, обычно когда асинхронная операция завершится. callback hell (pyramid of doom) — это когда колбэки вложены друг в друга для последовательных зависимых async-шагов, что даёт глубоко вложенный, плохо читаемый и подверженный ошибкам код. Промисы и async/await сглаживают эту вложенность.
Типичные ошибки
- ✗Думать, что колбэк всегда выполняется асинхронно — многие API вызывают колбэк синхронно
- ✗Считать callback hell проблемой производительности, а не читаемости и обработки ошибок
- ✗Полагать, что вложенность обязательна для последовательности async-шагов, хотя промисы её сглаживают
Уточняющие вопросы
- →Как соглашение error-first колбэков
(err, data)обрабатывает ошибки? - →Как именно промисы решают проблему читаемости callback hell?
MiddleТеорияЧастоМеханически, что возвращает async-функция и как await возобновляет работу?
Механически, что возвращает async-функция и как await возобновляет работу?
async-функция всегда возвращает промис: возвращённое значение выполняет его, выброшенная ошибка отклоняет. На await функция приостанавливается и возвращает управление вызывающему; когда ожидаемый промис разрешается, продолжение планируется как микрозадача и возобновляется там со значением (или выбрасывает причину). Поэтому код после await никогда не выполняется синхронно — он идёт в очереди микрозадач после текущей задачи.
Типичные ошибки
- ✗Думать, что
async-функция возвращает сырое значение, а не оборачивающий его промис - ✗Считать, что продолжение после
awaitвыполняется синхронно на том же стеке - ✗Полагать, что продолжение после
await— макрозадача, а не микрозадача
Уточняющие вопросы
- →Если
async-функция возвращает другой промис, через сколько тиков разрешится внешний? - →Почему синхронный блок кода полностью отрабатывает до любого продолжения
await?
MiddleТеорияЧастоКак обрабатывать ошибки с async/await и как промисы их проглатывают?
Как обрабатывать ошибки с async/await и как промисы их проглатывают?
Отклонённый ожидаемый промис выбрасывает исключение в точке await, поэтому его ловят обычным try/catch; синхронный throw внутри async-функции вместо этого отклоняет её возвращаемый промис. Ошибки проглатываются, когда вы забываете await или .catch() — отклонение остаётся необработанным. Решение — всегда await или цепочка .catch() на каждом промисе.
Типичные ошибки
- ✗Думать, что отклонённый ожидаемый промис возвращает
undefined, а не выбрасывает - ✗Забывать
awaitу промиса, оставляя его отклонение необработанным и проглоченным - ✗Считать, что
try/catchне ловит ошибки изawait, раз они асинхронные
Уточняющие вопросы
- →Почему отклонение промиса, который вы не
await, становится unhandled rejection? - →Как ведёт себя
try/catchвокругawait, чей промис отклоняется синхронно vs позже?
MiddleТеорияЧастоКак работает цепочка промисов и как она решает callback hell?
Как работает цепочка промисов и как она решает callback hell?
Каждый .then() возвращает новый промис, поэтому можно сцеплять .then().then() для последовательности async-шагов плоским списком вместо вложенных колбэков. Если обработчик возвращает промис, цепочка ждёт его и перенимает результат (выравнивание), передавая значение в следующий .then(). Один завершающий .catch() ловит любое отклонение в цепочке, заменяя разрозненную обработку ошибок в каждом колбэке.
Типичные ошибки
- ✗Забывать
returnвнутри.then(), что ломает поток значений в цепочке - ✗Думать, что возвращённый из обработчика промис остаётся вложенным, а не выравнивается
- ✗Считать, что ошибки нужно ловить в каждом
.then(), а не одним завершающим.catch()
Уточняющие вопросы
- →Что происходит с цепочкой, если обработчик
.then()синхронно выбрасывает исключение? - →Откуда
.catch()в середине цепочки возобновляет выполнение далее?
JuniorТеорияИногдаЧто такое observable в JavaScript?
Что такое observable в JavaScript?
Observable — это ленивый push-поток, который может выдать от 0 до многих значений во времени, плюс необязательный сигнал завершения или ошибки. Ничего не выполняется, пока вы не вызовете subscribe; каждая подписка запускает свой источник, и в ответ вы получаете способ отписаться, чтобы источник можно было отменить и освободить.
Типичные ошибки
- ✗Думать, что observable запускается сразу при создании, а не лениво при
subscribe - ✗Считать, что он несёт одно значение как промис, а не поток из многих
- ✗Забывать, что от него можно отписаться, чтобы отменить и освободить источник
Уточняющие вопросы
- →Что позволяет сделать функция, возвращённая из
subscribe? - →Почему каждая новая подписка заново запускает источник observable?
MiddleТеорияИногдаЧем observable отличается от промиса?
Чем observable отличается от промиса?
Промис нетерпелив (запускается при создании), завершается один раз одним значением и не отменяется. Observable ленив (запускается при subscribe), может выдать от 0 до многих значений во времени и отменяем через отписку. Промисы всегда разрешаются асинхронно в очереди микрозадач; observable может выдавать значения синхронно или асинхронно.
Типичные ошибки
- ✗Путать, кто ленив — ленив observable, а промис нетерпелив
- ✗Думать, что промис может отдать несколько значений как поток observable
- ✗Считать промис отменяемым, тогда как только observable поддерживает отписку
Уточняющие вопросы
- →Почему значение промиса кэшируется после завершения, а у observable — нет?
- →Как преобразовать одно-значный observable в промис?
MiddleТеорияИногдаЧем различаются Promise.all, race, allSettled и any?
Чем различаются Promise.all, race, allSettled и any?
Все принимают итерируемое из промисов. Promise.all выполняется массивом всех значений, но отклоняется при первом отклонении (fail-fast). Promise.race разрешается, как только разрешится первый — успешно или с ошибкой. Promise.allSettled ждёт все и даёт массив {status, value/reason}, никогда не отклоняясь. Promise.any выполняется первым успехом, отклоняясь AggregateError только если отклонились все.
Типичные ошибки
- ✗Думать, что
Promise.allвозвращает частичные результаты — одно отклонение отклоняет всё - ✗Путать
race(первый разрешившийся в любую сторону) сany(первый успешный) - ✗Считать, что
allSettledможет отклониться — он всегда выполняется с исходами по каждому промису
Уточняющие вопросы
- →Что делает
Promise.race, если первым разрешается отклонённый промис? - →Почему
Promise.anyотклоняетсяAggregateError, а не одной причиной?
SeniorТеорияИногдаКогда await должен быть последовательным, а когда параллельным, и как ограничить параллелизм?
Когда await должен быть последовательным, а когда параллельным, и как ограничить параллелизм?
await в цикле выполняет шаги последовательно — это уместно, лишь когда каждый зависит от предыдущего. Для независимой работы сначала запустите все промисы и await Promise.all([...]) для параллельного выполнения; учтите, что Promise.all fail-fast, поэтому используйте allSettled, если нужны все результаты. Для множества задач запускайте ограниченный пул (например, N воркеров, тянущих из очереди), чтобы использовать параллелизм, не перегружая ресурс.
Типичные ошибки
- ✗Использовать
awaitдля независимых вызовов в цикле, сериализуя то, что могло бы идти параллельно - ✗Думать, что
Promise.allвозвращает частичные результаты, а не падает быстро при первом отклонении - ✗Запускать тысячи промисов сразу без ограничения, исчерпывая соединения или память
Уточняющие вопросы
- →Почему запуск промисов до
await Promise.allважен для параллелизма? - →Как реализовать пул, ограничивающий параллелизм N выполняемыми задачами?
JuniorТеорияРедкоКак определить, является ли значение промисом или thenable?
Как определить, является ли значение промисом или thenable?
Надёжного instanceof Promise между realm'ами нет, поэтому спецификация использует утиную типизацию: значение считается thenable, если это не null-объект (или функция), у которого .then вызываемо. Promise.resolve(x) опирается на это — если x уже thenable, он перенимает его итоговое состояние, иначе оборачивает x в выполненный промис.
Типичные ошибки
- ✗Полагаться на
instanceof Promise, который ломается между realm'ами вроде iframe и воркеров - ✗Думать, что thenable только нативные промисы, тогда как подходит любой объект с вызываемым
.then - ✗Считать, что
Promise.resolveвсегда оборачивает заново, а он перенимает состояние готового thenable
Уточняющие вопросы
- →Почему
instanceof Promiseненадёжен через границу iframe? - →Что делает
Promise.resolve, если ему передать уже разрешённый нативный промис?
MiddleТеорияРедкоЧто такое thenable, и как await и then ассимилируют его?
Что такое thenable, и как await и then ассимилируют его?
Thenable — это любой объект с вызываемым методом .then(resolve, reject). Когда вы делаете await thenable или передаёте его в разрешение промиса, среда вызывает его .then, перенимая то, что он в итоге сигналит — так свой thenable может заменить настоящий промис. Ассимиляция рекурсивна: thenable, разрешающийся в другой thenable, разворачивается, пока не останется не-thenable значение.
Типичные ошибки
- ✗Думать, что awaited могут быть лишь нативные промисы, а годится любой объект с
.then - ✗Считать ассимиляцию одноуровневой, а не рекурсивным разворачиванием
- ✗Полагать, что
awaitчитает внутренний слот, а не реально вызывает.then
Уточняющие вопросы
- →Что произойдёт, если
.thenthenable вызовет иresolve, иreject? - →Почему вредоносный thenable может выполнить произвольный код при ассимиляции?