Що таке 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
Швидкий приклад
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 після обробки помилки
// Неправильно - виконання падає на блок з даними
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 у виробничі бази даних - код начебто працює, але псує записи.
Переплутані аргументи у власній функції
// Неправильно - зламає всіх, хто це викликає
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-коду
// Не дає нічого корисного
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.90mysqljs/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 на весь ланцюжок.
Приклади
Базовий: читання файлу з передачею помилки
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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.