async/await и обработка ошибок
Про async/await спрашивают всех, но интересные ответы начинаются на вопросе «а где здесь можно потерять ошибку».
async-функция всегда возвращает промис.awaitне блокирует поток: он приостанавливает выполнение конкретной функции и отдаёт управление event loop до тех пор, пока промис не разрешится.
Что проверяет интервьюер
Прежде всего — понимание, что async/await это синтаксический сахар над промисами, а не «магия синхронности». Дальше идёт практика: как ловить ошибки, что происходит с промисом без await, что случится с процессом при необработанном отказе и почему forEach ломается на асинхронных колбэках.
Базовая обработка ошибок
await превращает reject в обычное исключение, поэтому работает знакомый try/catch. Классы ошибок при этом остаются полностью в вашей власти — вот полностью синхронная иллюстрация того, как обычно устроен слой ошибок в веб-приложении:
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = 'HttpError';
this.status = status;
}
}
function parseAge(raw) {
const n = Number(raw);
if (!Number.isInteger(n) || n < 0) {
throw new HttpError(400, 'некорректный возраст: ' + raw);
}
return n;
}
for (const raw of ['30', '-1', 'abc']) {
try {
console.log('ок:', parseAge(raw));
} catch (e) {
if (e instanceof HttpError) console.log(e.status, e.message);
else throw e; // чужие ошибки не глотаем
}
}
Вывод:
ок: 30
400 некорректный возраст: -1
400 некорректный возраст: abc
Ключевая деталь, за которую цепляются на собеседовании: в catch нужно различать ошибки, а не глотать всё подряд. Блок catch (e) {} без разбора превращает баги в тишину.
Где теряются ошибки
async function saveOrder(order) {
await db.insert(order);
}
async function handler(req, res) {
saveOrder(req.body); // ЛОВУШКА: нет await
res.json({ ok: true }); // ответили «успех», хотя запись могла упасть
}
Промис без await и без .catch() — самая частая причина «молчаливых» багов в проде. Если он отклонится, начиная с Node 15 процесс по умолчанию завершается с ошибкой unhandledRejection. Раньше выводилось только предупреждение, и на собеседовании любят уточнять, как изменилось поведение.
Если запуск в фоне нужен намеренно, это надо писать явно:
// намеренно fire-and-forget, но с явной обработкой
void saveOrder(req.body).catch((err) => logger.error({ err }, 'saveOrder failed'));
// страховочная сетка процесса (логировать и выходить, а не «жить дальше»)
process.on('unhandledRejection', (err) => {
logger.fatal({ err }, 'unhandled rejection');
process.exit(1);
});
Ловушка forEach
// НЕ работает: forEach не ждёт промисы
items.forEach(async (item) => {
await save(item);
});
console.log('всё сохранено'); // печатается сразу, ничего ещё не сохранено
// последовательно — когда важен порядок или нельзя нагружать БД
for (const item of items) {
await save(item);
}
// параллельно — когда операции независимы
await Promise.all(items.map((item) => save(item)));
forEach просто вызывает колбэк и выбрасывает возвращённый промис. Ошибки внутри такого колбэка тоже никто не поймает. Правильных вариантов два — for...of с await для последовательной обработки и Promise.all для параллельной.
await в try, finally и возврат значения
async function withConnection(fn) {
const conn = await pool.connect();
try {
return await fn(conn); // именно await, а не просто return
} finally {
conn.release(); // выполнится и при ошибке, и при успехе
}
}
Здесь важен нюанс, который на интервью считается признаком опыта: return await внутри try нужен, чтобы ошибка из fn попала в текущий try/finally, а соединение освободилось до того, как промис уйдёт наверх. Без await функция вернёт промис слишком рано, и finally отработает раньше, чем завершится работа.
Типичные ошибки кандидатов
- Говорят, что
await«останавливает поток» — он приостанавливает только текущую функцию. - Забывают
awaitи удивляются, что ошибки не ловятся, а ответ уходит раньше записи в БД. - Пишут
async-колбэк вforEachи считают, что цикл дождётся операций. - Не знают, что необработанный reject с Node 15 роняет процесс.
- Ловят всё в
catchи возвращают 500 без логирования — ошибка исчезает.
Как ответить кратко
async-функция всегда возвращает промис, аawaitприостанавливает только её саму, отдавая управление event loop. Reject превращается в исключение, поэтому ловится обычнымtry/catch. Главные грабли — забытыйawait: ответ уходит раньше операции, ошибка не ловится, и с Node 15 необработанный reject завершает процесс. ВforEachасинхронные колбэки не ждутся: для последовательной обработки нуженfor...of, для параллельной —Promise.all.