Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як обробляти помилки в Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Обробка помилок в Express.js** працює по-різному для синхронного і асинхронного коду. Синхронні помилки Express перехоплює автоматично. Async помилки потребують try/catch і `next(err)`, щоб дістатись error middleware. ```js app.get('/user/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); res.json(user); } catch (err) { next(err); } }); app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }); ``` **Ключове:** error middleware потребує рівно 4 параметри `(err, req, res, next)` і реєструється останнім.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Обробка помилок в Express.js** - механізм, який перехоплює помилки з обробників маршрутів і передає їх спеціальному error middleware, що формує відповідь замість того, щоб крашити процес. ## Теорія ### TL;DR - Синхронні помилки в маршрутах Express перехоплює автоматично - Async помилки (promise, await) НЕ перехоплюються автоматично - потрібен try/catch і `next(err)` - Error middleware має рівно 4 параметри `(err, req, res, next)` і реєструється останнім - Обгортка `asyncHandler` позбавляє від повторення try/catch у кожному маршруті - В Node 15+ необроблений rejection крашить процес ### Швидкий приклад ```js // НЕПРАВИЛЬНО: async помилка обходить error handling Express app.get('/user/:id', async (req, res) => { const user = await User.findById(req.params.id); // rejection не перехоплено res.json(user); }); // ПРАВИЛЬНО: перехоплюємо і передаємо в error middleware app.get('/user/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); res.json(user); } catch (err) { next(err); // направляє в error-handling middleware } }); // Error middleware - 4 параметри, реєструється останнім app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }); ``` Express бачить 4-параметровий підпис і розуміє, що це обробник помилок, а не звичайний middleware. ### Синхронні vs асинхронні помилки Express загортає синхронні обробники маршрутів у внутрішній блок try/catch. Коли ти `throw`-иш у синхронному обробнику, Express перехоплює помилку і передає її далі по ланцюгу error middleware. З async обробниками інша картина. Коли викликається `async` функція, вона одразу повертає Promise. Express викликає обробник, отримує Promise і рухається далі. Якщо той Promise відхиляється пізніше, Express вже "пішов" від цього обробника - rejection відбувається поза межами його try/catch. Node.js генерує подію `unhandledRejection`, і в Node 15+ це завершення процесу. Саме тут найчастіше виникають непомітні збої в Express-застосунках. Один пропущений try/catch у популярному маршруті - і сервер падає. ### Коли що використовувати - **Синхронний код у маршруті** - `throw` freely, Express перехоплює сам - **async/await у маршруті** - обгорни в try/catch, у catch блоці виклич `next(err)` - **Callbacks (старий код)** - виклич `next(err)` в error-шляху callback - **Багато async маршрутів** - використовуй обгортку `asyncHandler`, щоб не писати try/catch скрізь - **Неопрацьовані маршрути (404)** - додай catch-all middleware після всіх маршрутів, перед error handler ### Патерн asyncHandler Писати try/catch у кожному маршруті швидко набридає. Обгортка `asyncHandler` вирішує це: ```js // Обгортка перехоплює rejected promises і передає в next() const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; // try/catch у маршруті більше не потрібен app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) { const err = new Error('Not found'); err.status = 404; throw err; // обгортка перехопить і викличе next(err) } res.json(user); })); ``` Обгортка резолвить повернутий Promise і, якщо він відхиляється, передає помилку напряму в `next`. Саме цей патерн використовують бібліотеки `express-async-errors` та `express-async-handler` на npm. ### Власні класи помилок Для простих випадків достатньо додати `status` до звичайного `Error`. Але власні класи помилок роблять обробку структурованішою при зростанні застосунку: ```js class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; // прапор для "очікуваних" помилок Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(message) { super(message, 400); } } ``` В error middleware перевіряй `err instanceof NotFoundError`, щоб по-різному обробляти різні типи помилок. Операційні помилки (поганий input, відсутній ресурс) несуть власні статус-коди. Несподівані помилки отримують 500. ### Як Express розпізнає error middleware Express визначає, чи є middleware обробником помилок, перевіряючи властивість `.length` функції - кількість оголошених параметрів. Звичайний middleware має 2 або 3 параметри. Error middleware - рівно 4. Напишеш `(err, req, res)` - 3 параметри - Express сприйме це як звичайний middleware і пропустить для помилок. Тому параметр `next` в error middleware потрібно завжди оголошувати, навіть якщо ніколи не викликаєш його. ### Типові помилки **Помилка 1: async обробник без try/catch** ```js // НЕПРАВИЛЬНО: rejection не перехоплено, Node 15+ завершить процес app.get('/data', async (req, res) => { const data = await fetchData(); res.json(data); }); // ПРАВИЛЬНО app.get('/data', async (req, res, next) => { try { const data = await fetchData(); res.json(data); } catch (err) { next(err); } }); ``` Express загортає тільки синхронну частину обробника. Async частина виконується після того, як обробник вже повернув значення. **Помилка 2: error middleware зареєстровано перед маршрутами** ```js // НЕПРАВИЛЬНО: обробник помилок реєструється першим - маршрути його не досягнуть app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); app.get('/users', async (req, res, next) => { /* ... */ }); // ПРАВИЛЬНО: спочатку маршрути, потім error handler app.get('/users', async (req, res, next) => { /* ... */ }); app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); ``` Express обробляє middleware в порядку реєстрації. Error handler, зареєстрований до маршрутів, ніколи не буде в ланцюзі виконання цих маршрутів. **Помилка 3: виклик next() до async операції** ```js // НЕПРАВИЛЬНО app.get('/user/:id', async (req, res, next) => { next(); // каже Express "тут закінчив" const user = await User.findById(req.params.id); // помилка тут - без обробника res.json(user); }); ``` Як тільки `next()` викликано без аргументу помилки, Express переходить до наступного middleware. Будь-яка помилка після цього не має обробника. **Помилка 4: одночасна відправка відповіді і передача помилки** ```js // НЕПРАВИЛЬНО: і res.json(), і next(err) можуть виконатись одночасно app.get('/user/:id', async (req, res, next) => { const user = await User.findById(req.params.id).catch(next); res.json(user); // виконається навіть якщо .catch(next) вже спрацював }); // ПРАВИЛЬНО: або відправляємо відповідь, або передаємо помилку app.get('/user/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); } catch (err) { next(err); } }); ``` Після виклику `res.json()` або `res.send()` відповідь відправлено. Повторний виклик дає "Cannot set headers after they are sent". **Помилка 5: однакова обробка всіх помилок** ```js // НЕПРАВИЛЬНО: все стає 500 app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); // ПРАВИЛЬНО: беремо статус з об'єкта помилки app.use((err, req, res, next) => { const status = err.statusCode || err.status || 500; const message = err.message || 'Internal server error'; if (!err.isOperational) { console.error('Unexpected error:', err); // логуємо програмні помилки } res.status(status).json({ error: { status, message }, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); ``` Операційні помилки (NotFoundError, ValidationError) несуть власні статус-коди і є очікуваними. Програмні помилки потрібно логувати і повертати 500. Однакова обробка ховає реальні баги. ### Де зустрічається в реальних проектах - **Express-застосунки** - async маршрути потребують try/catch + `next(err)` або обгортки asyncHandler - **express-async-errors** - npm пакет, який патчить Express для автоматичної обробки async помилок - **NestJS** - використовує exception filters замість middleware, та сама ідея з іншим синтаксисом - **Fastify** - вбудована обробка async помилок, обгортка не потрібна - **Koa** - використовує try/catch прямо в middleware, без патерну `next(err)` ### Follow-up питання **Q:** Чому Express не перехоплює помилки в async обробниках автоматично? **A:** Express загортає синхронне виконання обробника в try/catch. Async функція одразу повертає Promise, тому до моменту, коли Promise відхиляється, обробник вже повернув значення і try/catch закрився. Rejection відбувається поза областю видимості Express. **Q:** Що відбувається з необробленими promise rejection у Node 15+? **A:** Node.js завершує процес. До Node 15 це видавало лише deprecation попередження. Тому залишати async помилки без обробки в Express - це не просто погана практика, це краш продакшену. **Q:** Чи можна мати кілька error-handling middleware функцій? **A:** Так. Express викликає їх по порядку, поки одна не припинить викликати `next(err)`. Можна мати спеціалізовані обробники для validation і database помилок перед загальним catch-all. Кожен має оголошувати рівно 4 параметри. **Q:** Що відбувається, якщо error middleware сам кидає помилку? **A:** Express перехоплює її і відправляє загальну відповідь 500. Error middleware має бути захищеним: перевіряй властивості `err`, використовуй значення за замовчуванням, не викликай зовнішній код без захисту. **Q:** Як організувати обробку помилок у великому застосунку з різними доменами (auth, users, payments)? **A:** Створи власні класи помилок для кожного домену - `AuthenticationError`, `PaymentError`, `ValidationError` - кожен з властивістю `statusCode`. В маршрутах `throw` потрібний клас. У глобальному error handler перевіряй `instanceof` для різного форматування: `AuthenticationError` повертає 401 без внутрішніх деталей, `ValidationError` - 400 з описом полів, несподівані помилки - 500 і запис у лог. Це відокремлює логіку форматування від бізнес-логіки і робить кожен error path тестованим. ## Приклади ### Базовий: синхронна vs асинхронна обробка помилок ```js const express = require('express'); const app = express(); // Sync: Express перехоплює автоматично app.get('/sync', (req, res) => { throw new Error('Something went wrong'); // try/catch не потрібен }); // Async: потрібно перехопити самостійно і передати в next app.get('/async', async (req, res, next) => { try { const data = await fetchSomething(); res.json(data); } catch (err) { next(err); // передаємо в error middleware } }); // Error middleware - завжди останній, завжди 4 параметри app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }); app.listen(3000); ``` Синхронний `throw` одразу потрапляє в error middleware. Async помилка без try/catch крашить процес в Node 15+. ### Середній: продакшн маршрут з валідацією і помилками бази даних ```js app.post('/users', async (req, res, next) => { try { // операційна помилка - поганий input від клієнта if (!req.body.email) { const err = new Error('Email is required'); err.status = 400; throw err; } const user = await User.create(req.body); res.status(201).json(user); } catch (err) { next(err); // всі помилки йдуть в error handler } }); app.use((err, req, res, next) => { const status = err.status || 500; const message = err.message || 'Internal server error'; res.status(status).json({ error: { status, message }, // stack видно лише в розробці ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); ``` Помилки валідації повертають 400. Несподівані помилки бази даних - 500. Stack trace видно лише в режимі розробки. ### Просунутий: повне налаштування з власними класами помилок і asyncHandler ```js // Власні класи помилок class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(message) { super(message, 400); } } // Обгортка asyncHandler - без try/catch у маршрутах const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // Маршрут без boilerplate app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); // обгортка передасть в next() res.json(user); })); // 404 для неопрацьованих маршрутів - після всіх маршрутів app.use((req, res, next) => { next(new NotFoundError(`Route ${req.method} ${req.path}`)); }); // Глобальний error handler - завжди останній app.use((err, req, res, next) => { const status = err.statusCode || err.status || 500; if (!err.isOperational) { console.error('Unexpected error:', err); // логуємо несподівані помилки } res.status(status).json({ error: err.message, ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) }); }); ``` `isOperational` позначає помилки, які ти очікуєш (поганий input, відсутні записи). Несподівані помилки логуються з повним stack trace перед тим, як клієнту йде загальний 500.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.