Express / 38 минут
Асинхронные обработчики Express 5
Используйте Promise-ошибки Express 5 и корректно связывайте асинхронную работу с запросом.
- Используйте Promise-ошибки Express 5 и корректно связывайте асинхронную работу с запросом.
- Применить тему в задаче «Асинхронные обработчики Express 5» и объяснить граничные случаи.
Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Незавершённая операция внутри HTTP-сценария
Обработчик часто ждёт БД, внешний HTTP-сервис или файловую операцию. Он должен связать завершение этой работы с конкретным запросом. async/await помогает выразить последовательность, но не решает автоматически вопросы timeout, отмены, повторов и владения результатом. Если клиент ушёл, приложение всё равно должно понимать, что делать с уже запущенной операцией.
Не выполняйте тяжёлый синхронный расчёт в надежде, что async в объявлении переместит его на другой поток. До и после await обычный JavaScript занимает event loop. Дорогую CPU-работу и тем более произвольный клиентский код выносят в подходящий изолированный механизм. Серверный HTTP-процесс должен сохранять предсказуемую стоимость запроса.
Возвращаемый Promise и ошибка
В Express 5 handler может вернуть Promise, и его отказ будет передан в ошибочную цепочку. Если внутри async-функции бросить Error после await, централизованный handler должен получить её без ручной обёртки каждого маршрута. Это важное отличие, которое нужно учитывать при чтении старых примеров Express 4.
app.get('/value', async (req, res) => {
const value = await Promise.resolve(42);
return res.json({ value });
});app.get('/failure', async () => {
await Promise.resolve();
throw new Error('private detail');
});При этом Promise должен принадлежать возвращаемой цепочке. Запуск внешней функции без await или return способен отделить её отказ от HTTP-сценария. Такой фоновый процесс может быть допустим только при отдельной явной ответственности: очереди задач, обработке ошибок и жизненном цикле, а не как случайно забытое ожидание.
Последовательность и конкурентное ожидание
Если вторая операция использует результат первой, их выполняют последовательно. Независимые чтения можно объединить через Promise.all, учитывая общий отказ и ограничения нагрузки. Promise.all не отменяет остальные запущенные операции автоматически при первой ошибке. Для многих внешних запросов может понадобиться ограниченная конкурентность, а не запуск всего набора сразу.
app.get('/dashboard', async (_req, res) => {
const [courses, progress] = await Promise.all([
catalog.list(), progressService.current(),
]);
res.json({ courses, progress });
});В этом фрагменте catalog и progressService — заранее внедрённые зависимости. Не создавайте новое неограниченное подключение к БД на каждый handler. Также убедитесь, что current получает подтверждённого пользователя правильного запроса, а не читает случайную общую переменную процесса.
Timeout и отмена
Внешняя операция должна иметь ограничение ожидания, соответствующее контракту. Современный fetch поддерживает AbortSignal; конкретный клиент БД или библиотека может иметь собственные механизмы. Отмена HTTP-клиента не гарантирует автоматический rollback уже выполненной бизнес-операции, поэтому её нельзя считать универсальной отменой эффекта.
async function requestInfo(url) {
const response = await fetch(url, { signal: AbortSignal.timeout(3000) });
if (!response.ok) throw new Error('upstream_failed');
return response.json();
}URL здесь должен приходить из доверенной конфигурации или проходить отдельную ограничивающую проверку, а не быть произвольным адресом от клиента. Серверный fetch к неограниченному пользовательскому URL создаёт другую границу риска. Успешное ожидание JSON также не подтверждает его схему.
Ответ должен завершаться один раз
После успешного res.json не продолжайте ветку, которая способна отправить ещё один ответ. Для условного отказа используйте ранний return. Если заголовки уже отправлены, error middleware должен учитывать это состояние. Ошибка «headers already sent» часто является следствием неправильного управления потоком, а не поводом добавить случайный setTimeout.
Если несколько запросов изменяют один ресурс, await сам по себе не обеспечивает взаимного исключения или транзакции. Между чтением и записью может завершиться другой запрос. Защита от конфликтов находится в модели хранения: уникальные ограничения, версии, транзакции и обоснованные уровни изоляции.
Практика успешной и ошибочной ветви
В задаче async GET /value после асинхронного шага возвращает {value} для неотрицательного числа и бросает Error для отрицательного. Error middleware отвечает безопасным {error:'internal'} и 500. Проверяются обе ветви через реальный HTTP-запрос, поэтому забытый await или неверный middleware проявится в результате.
Проверьте положительное значение, ноль и несколько отрицательных чисел. Отдельно убедитесь, что исходное private detail не появляется в ответе. Для самостоятельной тренировки внедрите fake-репозиторий, который может успешно завершиться или отклонить Promise, и напишите по одному интеграционному тесту каждого публичного сценария без настоящего внешнего сервера.