Skip to main content

Що таке error-first callback патерн?

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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?