Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Шаблони обробки помилок у Node.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Шаблони обробки помилок у Node.js** - конвенції для перехоплення та передачі помилок у коді на колбеках, Promise та async/await. ```js async function getUser(id) { try { return await db.users.findById(id); } catch (err) { console.error(err.message); throw err; // перекидаємо, щоб error middleware сформував відповідь } } ``` **Ключове:** завжди перекидай помилку з async-функції, якщо не повертаєш конкретний fallback, інакше caller отримає `undefined` замість помилки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Шаблони обробки помилок у Node.js** - набір конвенцій для перехоплення та передачі помилок у коді на колбеках, Promise та async/await, щоб один необроблений збій не завалив увесь сервер. ## Теорія ### Коротко - Дві категорії: **операційні помилки** (таймаут БД, файл не знайдено) - обробляєш і відновлюєшся; **помилки програміста** (TypeError, неправильний аргумент) - виправляєш і рестартуєш - Колбеки використовують конвенцію error-first: перший аргумент завжди `err` або `null` - Promise використовують `.catch()`, async-функції - `try/catch` - В Express потрібен middleware з 4 аргументами `(err, req, res, next)` після всіх роутів - `uncaughtException` зобов'язаний викликати `process.exit(1)` - стан процесу після цієї події пошкоджено ### Швидкий приклад Найпоширеніший сучасний підхід: ```js async function loadUserProfile(userId) { try { const user = await db.users.findById(userId); if (!user) { throw new NotFoundError('User'); // власна помилка з кодом статусу } return user; } catch (err) { logger.error({ userId, err }, 'Failed to load user profile'); throw err; // перекидаємо, щоб Express middleware сформував відповідь } } ``` Один `try/catch` накриває всі `await` у функції. Будь-яке відхилення потрапляє в `catch`. Окремий `.catch()` на кожен виклик не потрібен. ### Операційні помилки vs помилки програміста Це розрізнення часто виникає на співбесідах. Операційні помилки (operational errors) - очікувані: БД відключилась, юзер надіслав невалідні дані, файл не існує. Ти їх передбачаєш і пишеш код відновлення: повертаєш 404, робиш retry, повертаєш повідомлення валідації. Помилки програміста - це баги. `TypeError: Cannot read properties of undefined` означає, що код невірний. Їх не "обробляють" gracefully. Логують, завершують процес і дають PM2 запустити сервер знову. Саме тут і починаються більшість продакшн-інцидентів - коли плутають одне з одним. ### Колбеки: error-first конвенція До появи Promise кожен асинхронний API в Node.js використовував цю конвенцію: ```js const fs = require('fs'); fs.readFile('./config.json', 'utf8', (err, data) => { if (err) { // err.code: 'ENOENT' - файл не знайдено, 'EACCES' - немає прав console.error('Could not read config:', err.code); return; // зупинити - data тут undefined } const config = JSON.parse(data); startServer(config); }); ``` Правило: завжди перевіряй `err` першим і завжди роби `return` після обробки. Пропустиш `return` - код нижче виконається з `undefined` у `data`. Цей патерн і досі зустрічається в Node.js core API (`fs`, `dns`, `crypto`) та старих кодових базах. ### Promise та .catch() Помилки в ланцюжку Promise передаються далі до першого `.catch()`: ```js fetchUser(id) .then(user => enrichWithPosts(user)) // відхилення тут перескочить до .catch .then(enriched => formatResponse(enriched)) .catch(err => { console.error('Pipeline failed:', err.message); return { error: err.message }; // повертаємо fallback для відновлення }) .finally(() => metrics.record('fetchUser')); // виконується завжди ``` `.finally()` зручний для cleanup, який має відбутись незалежно від результату: закриття з'єднань, запис метрик, звільнення ресурсів. ### async/await та try/catch `async/await` - це синтаксичний цукор над Promise. Модель помилок та сама, але код виглядає як синхронний: ```js async function processOrder(orderId) { let order; try { order = await db.orders.findById(orderId); } catch (err) { throw new DatabaseError('Could not fetch order', err); } try { await paymentService.charge(order.totalAmount); } catch (err) { throw new PaymentError('Charge failed', err); } return order; } ``` Окремі блоки `try/catch` дають різні типи помилок для різних кроків. Один великий `try/catch` теж нормально, якщо потрібна однакова обробка всіх збоїв. ### Middleware для помилок в Express Express визначає error middleware за сигнатурою з 4 параметрами. Ставиться після всіх роутів: ```js // Обгортка для async-роутів: перекидає помилки в next() function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); })); // Централізований обробник помилок - останній middleware app.use((err, req, res, next) => { const status = err.statusCode || 500; res.status(status).json({ error: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, }); }); ``` Без `asyncHandler` помилки з async-роутів ніколи не потраплять до error middleware. Обгортка викликає `.catch(next)`, що передає помилку в pipeline Express. ### Власні класи помилок Кинути `new Error('not found')` - робочий варіант. Але HTTP API потребують кодів статусу, а error middleware має відрізняти твої помилки від несподіваних збоїв у сторонніх бібліотеках: ```js class AppError extends Error { constructor(message, statusCode = 500) { super(message); this.statusCode = statusCode; this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); // стек з місця throw } } class NotFoundError extends AppError { constructor(resource) { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(field, issue) { super(`${field}: ${issue}`, 422); this.field = field; } } ``` Error middleware перевіряє `err instanceof AppError`, щоб відрізнити очікувані помилки від сюрпризів. `Error.captureStackTrace` залишає стек-трейс таким, що вказує на місце виклику `throw`, а не на конструктор. ### Глобальні обробники процесу Це остання лінія захисту для всього, що пройшло крізь усі `try/catch` та `.catch()`: ```js // Node.js 15+ падає за замовчуванням при unhandledRejection // Обробник дає можливість залогувати перед виходом process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection:', reason); process.exit(1); }); // Синхронний throw, що вийшов за межі всіх try/catch process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); process.exit(1); // обов'язково - стан процесу після цього невизначений }); ``` Я бачив команди, що додавали `process.on('uncaughtException')` вважаючи, що це робить сервер стійким до збоїв. Ні. Це ховає краші і запускає зламаний код. Нормальне покриття `try/catch` і process manager, який рестартує сервер при виході - ось реальний захист. ### Типові помилки **Помилку ковтають у async-функції:** ```js // Неправильно - caller отримує undefined, помилка зникає async function getUser(id) { try { return await fetchUser(id); } catch (err) { console.error(err); // логується, але не перекидається } } // Правильно async function getUser(id) { try { return await fetchUser(id); } catch (err) { console.error(err); throw err; } } ``` **Забули asyncHandler в Express:** ```js // Неправильно - відхилення ніколи не потрапить до error middleware app.get('/user/:id', async (req, res) => { const user = await User.findById(req.params.id); // unhandled rejection res.json(user); }); ``` **Promise.all() там, де допустима часткова відмова:** ```js // Якщо хоч один падає - падають усі const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]); // Promise.allSettled() повертає результат кожного окремо const results = await Promise.allSettled([fetchUsers(), fetchPosts()]); const users = results[0].status === 'fulfilled' ? results[0].value : []; const posts = results[1].status === 'fulfilled' ? results[1].value : []; ``` **Надто широкий catch:** ```js // Ловить TypeError з багів так само, як операційні помилки try { const data = await fetchData(); processData(data); // баг тут дасть 500 без жодної інформації } catch (err) { res.status(500).json({ error: 'Something went wrong' }); } ``` Окремий `try/catch` для fetch і обробка результату поза ним - або перевірка `err instanceof` для різних типів збоїв. ### Де зустрічається в реальному коді - Express API: `asyncHandler` на кожному роуті і один error middleware в кінці - Шар бази даних: ловиш конкретні коди помилок (Postgres `23505` для duplicate) і кидаєш `ValidationError` - Зовнішні API: `axios` відхиляє на не-2xx відповідях, перевіряєш `err.response.status` у catch - Робота з файлами: `err.code === 'ENOENT'` відрізняє відсутній файл від помилки прав доступу - Паралельне завантаження даних: `Promise.allSettled()` коли кожна частина необов'язкова ### Питання на співбесіді **Q:** У чому різниця між `unhandledRejection` та `uncaughtException`? **A:** `unhandledRejection` спрацьовує коли відхилений Promise не має `.catch()`. `uncaughtException` - коли синхронний `throw` вийшов за межі всіх `try/catch`. Обидва вимагають `process.exit(1)`. **Q:** Чому `uncaughtException` зобов'язаний викликати `process.exit(1)`? **A:** Після uncaught exception стан процесу невизначений: відкриті з'єднання, таймери, I/O в польоті - все може бути в неконсистентному стані. Продовжувати обробляти запити означає повертати потенційно пошкоджені дані. Вихід і рестарт завжди безпечніший. **Q:** Як Express знаходить error middleware серед звичайних? **A:** Express перевіряє властивість `.length` функції. Middleware з рівно 4 параметрами `(err, req, res, next)` вважається обробником помилок. Коли викликається `next(err)`, Express пропускає всі звичайні middleware до першого з 4 параметрами. **Q:** Що робить `Error.captureStackTrace(this, this.constructor)` у власному класі помилки? **A:** Встановлює стек-трейс починаючи з місця виклику `throw`, а не з середини конструктора `AppError`. Без цього вершина кожного стеку показує конструктор, що не допомагає знайти джерело помилки. **Q:** Як отримати часткові результати коли одна з паралельних операцій падає? **A:** Заміни `Promise.all()` на `Promise.allSettled()`. Він завжди резолвиться масивом де кожен елемент має `status: 'fulfilled'` і `value`, або `status: 'rejected'` і `reason`. Фільтруєш за статусом і дістаєш що пройшло. ## Приклади ### Async роут з централізованою обробкою помилок ```js const express = require('express'); const app = express(); class AppError extends Error { constructor(message, statusCode = 500) { super(message); this.statusCode = statusCode; Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource) { super(`${resource} not found`, 404); } } function asyncHandler(fn) { return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); } app.get('/orders/:id', asyncHandler(async (req, res) => { const order = await Order.findById(req.params.id); if (!order) throw new NotFoundError('Order'); res.json(order); })); // Після всіх роутів app.use((err, req, res, next) => { res.status(err.statusCode || 500).json({ error: err.message }); }); ``` Роут-обробники залишаються чистими. Все формування відповідей на помилки в одному місці. ### Обгортання callback API через util.promisify ```js const fs = require('fs'); const { promisify } = require('util'); const readFile = promisify(fs.readFile); async function loadConfig(filePath) { try { const raw = await readFile(filePath, 'utf8'); return JSON.parse(raw); } catch (err) { if (err.code === 'ENOENT') { // операційна помилка - файл ще не існує, повертаємо дефолти return { port: 3000, debug: false }; } // несподівана помилка - JSON parse або права доступу - перекидаємо throw err; } } ``` Перевірка `err.code` відрізняє очікуваний кейс (файл ще не створено) від реального збою (невалідний JSON, помилка диску). ### Promise.allSettled() для дашборду ```js async function getDashboardData(userId) { const [ordersResult, profileResult, notificationsResult] = await Promise.allSettled([ fetchOrders(userId), fetchProfile(userId), fetchNotifications(userId), ]); return { orders: ordersResult.status === 'fulfilled' ? ordersResult.value : [], profile: profileResult.status === 'fulfilled' ? profileResult.value : null, notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value : [], errors: [ordersResult, profileResult, notificationsResult] .filter(r => r.status === 'rejected') .map(r => r.reason?.message), }; } ``` Дашборд завантажується навіть якщо один сервіс недоступний. Кожна секція або повертає реальні дані, або безпечний fallback. Поле `errors` повідомляє клієнту, що саме не вдалось.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.