Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке error-first callback патерн?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Error-first callback** - конвенція Node.js: async-функції передають `null` або `Error` першим аргументом callback, і результат другим. `fs.readFile(path, (err, data) => { if (err) return; })`. Завжди перевіряй `err` перед тим як використовувати `data`, і завжди роби `return` після обробки помилки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Error-first callback патерн** - конвенція Node.js, де async-функції завжди передають помилку першим аргументом у callback, і результат другим, тільки якщо помилки немає. ## Теорія ### TL;DR - Уяви кур'єра: спочатку він повідомляє, чи щось пішло не так (помилка), а потім віддає посилку (дані) тільки якщо все гаразд - Конвенція: `callback(err, data)` - спочатку помилка, потім дані - Успіх: `err` дорівнює `null`, `data` містить результат; помилка: `err` це об'єкт `Error`, `data` це `undefined` - Завжди перевіряй `if (err)` перед тим як використовувати `data`, і завжди роби `return` після - пропущений `return` це баг номер один - Використовуй для `fs`, `mysql`, `pg` та схожих бібліотек; для нового коду краще Promises ### Швидкий приклад ```javascript const fs = require('fs'); fs.readFile('config.json', 'utf8', (err, content) => { if (err) { console.error('Не вдалось прочитати:', err.message); // ENOENT: no such file... return; // Зупиняємо виконання тут } console.log(content); // { port: 3000 } - тільки якщо помилки немає }); ``` Помилка приходить першою. Якщо файл не знайдено, `err` буде встановлено, а `content` буде `undefined`. `return` не дає решті callback виконатись з пошкодженими даними. ### Чому помилка йде першою У синхронному коді ти кидаєш виняток, і десь вище по стеку його ловить try-catch. З async-кодом це не працює. Callback виконується пізніше, в окремому тіку event loop, і оригінальний try-catch вже давно завершив роботу. Помилка першим аргументом змушує тебе обробляти її вручну щоразу. Пропустити її просто так не вийде. ### Коли використовувати - Читання або запис файлів через `fs`: error-first callbacks - Запити до БД через `mysql` або `pg`: обидві бібліотеки дотримуються цієї конвенції - Своя async-функція: передавай `(err, result)`, щоб відповідати stdlib Node.js - Обгортання застарілої сторонньої бібліотеки: error-first тримає все однорідним - Новий код на Node 10+: краще `fs.promises` та async/await ### Як це працює всередині Node.js Потоки libuv керують I/O операціями, такими як `fs.readFile`. Коли операція завершується, libuv передає C-рівневий код помилки (наприклад, `UV_ENOENT`) або нуль назад до V8. Event loop потім викликає твій callback або з об'єктом `Error`, або з `null`. Сам патерн - це конвенція, runtime її не контролює, тому і забутий `return` так легко проскакує непоміченим. ### Типові помилки **Забутий `return` після обробки помилки** ```javascript // Неправильно - виконання падає на блок з даними fs.readFile('bad.txt', (err, data) => { if (err) console.error(err); // Немає return! console.log(data); // Все одно виконається - виведе undefined }); // Правильно fs.readFile('bad.txt', (err, data) => { if (err) { console.error(err); return; } console.log(data); }); ``` Без `return` виконання продовжується з `data === undefined`. Я бачив, як це призводило до тихого запису `undefined` у виробничі бази даних - код начебто працює, але псує записи. **Переплутані аргументи у власній функції** ```javascript // Неправильно - зламає всіх, хто це викликає function getUser(id, cb) { db.query(sql, [id], (err, row) => cb(row, err)); // аргументи переставлені! } // Правильно function getUser(id, cb) { db.query(sql, [id], (err, row) => cb(err, row)); } ``` Всі бібліотеки Node.js очікують `(err, data)`. Переставиш місцями - і ті, хто викликає функцію, перевіряють не той аргумент. **try-catch навколо async-коду** ```javascript // Не дає нічого корисного try { fs.readFile('file.txt', (err, data) => { throw new Error('boom'); // Тут не перехопиться }); } catch (e) { console.error(e); // Ніколи не виконається } ``` try-catch завершує роботу ще до того, як callback спрацьовує. Помилки треба обробляти всередині самого callback. ### Де зустрічається - `fs.readFile`, `fs.writeFile`: ядро Node.js, ця конвенція з версії v0.1.90 - `mysqljs/mysql`: `connection.query(sql, params, (err, rows) => {})` - понад 10M завантажень на тиждень - `pg` (Postgres): `client.query(sql, (err, result) => {})` - у більшості Node.js-бекендів - Express: сигнатура `(err, req, res, next)` для error middleware - прямий розвиток цієї конвенції - Конвертація у Promise: `util.promisify(fs.readFile)('file.txt').then(...).catch(...)` ### Питання на співбесіді **Q:** Чому Node.js не кидає звичайні винятки для async-помилок? **A:** Async callbacks виконуються після того, як оригінальний стек викликів вже зник. Виняток, кинутий у callback, нікому не передасться і впаде у процес. Error-first тримає контроль усередині callback. **Q:** Як перетворити error-first callback на Promise? **A:** Через `util.promisify`: `const readFile = util.promisify(fs.readFile)`. Далі використовуй `.then().catch()` або async/await. **Q:** Що якщо треба повернути кілька результатів? **A:** Передай об'єкт або масив другим аргументом: `cb(null, { user, token })`. Одна помилка, один результат - кілька значень пакуй разом. **Q:** Як у ланцюжку вкладених callbacks уникнути `if (err) return cb(err)` на кожному кроці? **A:** Виноси кожен крок у окрему іменовану функцію або використовуй `waterfall` із бібліотеки `async`. Для нового коду async/await вирішує це одним try-catch на весь ланцюжок. ## Приклади ### Базовий: читання файлу з передачею помилки ```javascript const fs = require('fs'); function readConfig(path, cb) { fs.readFile(path, 'utf8', (err, content) => { if (err) return cb(err); // Передаємо помилку вище cb(null, JSON.parse(content)); // Передаємо результат при успіху }); } readConfig('config.json', (err, config) => { if (err) { console.error('Не вдалось завантажити конфіг:', err.message); // ENOENT... return; } console.log('Порт:', config.port); // Порт: 3000 }); ``` `cb(err)` передає помилку вгору, не приховуючи її. `cb(null, result)` сигналізує про успіх. Той, хто викликає функцію, сам вирішує що робити з кожним варіантом. ### Середній: запит до Postgres у Express ```javascript const express = require('express'); const pg = require('pg'); const app = express(); app.get('/user/:id', (req, res) => { const client = new pg.Client(); client.connect((err) => { if (err) return res.status(500).json({ error: err.message }); client.query( 'SELECT * FROM users WHERE id = $1', [req.params.id], (err, result) => { client.end(); if (err) return res.status(500).json({ error: err.message }); res.json(result.rows[0]); // { id: 1, name: 'Alice' } } ); }); }); ``` Кожен async-крок перевіряє `err` перед тим як рухатись далі. `return` на кожному шляху помилки не дає відправити відповідь двічі. Це класичне callback hell - саме через яке і з'явились Promises.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.