Callbacks — перша парадигма асинхронності
Callbacks — перша парадигма асинхронності
🎯 Мета лекції
- Опанувати концепцію callback-функцій як механізм асинхронної комунікації.
- Зрозуміти Error-First Callback pattern — стандарт Node.js до появи промісів.
- Навчитися розпізнавати та уникати антипаттерну Callback Hell (Pyramid of Doom).
- Дослідити проблеми обробки помилок та контролю потоку виконання у callback-based коді.
- Розуміти історичний контекст переходу від callbacks до промісів та async/await.
🔑 Ключові терміни
- Callback (функція зворотного виклику): функція, що передається як аргумент іншій функції для виконання після завершення асинхронної операції.
- Error-First Callback: конвенція Node.js, де перший параметр callback завжди містить помилку (
nullу разі успіху), а другий — результат операції. - Callback Hell (пекло колбеків): антипаттерн, коли вкладені callback призводять до важкочитабельного коду з глибокою індентацією.
- Inversion of Control (інверсія керування): ситуація, коли контроль над виконанням коду передається зовнішній функції через callback.
Історичний контекст: асинхронність у ранніх версіях Node.js
Коли Раян Даль (Ryan Dahl) представив Node.js у 2009 році, мова JavaScript ще не мала вбудованої підтримки промісів (Promises) на рівні стандарту ECMAScript. Специфікація ES6 (ECMAScript 2015), яка офіційно додала Promise до мови, з'явилася лише у червні 2015 року. До цього моменту єдиним загальноприйнятим механізмом для роботи з асинхронним кодом у JavaScript були callback-функції (функції зворотного виклику).
Цей підхід був успадкований з браузерного JavaScript, де callback використовувалися для обробки подій DOM (Document Object Model), таймерів та AJAX-запитів. Наприклад, у браузері:
// Браузерний приклад (до появи fetch API)
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.onload = function() {
// Callback, що виконується після завершення запиту
if (xhr.status === 200) {
console.log('Дані отримано:', xhr.responseText);
}
};
xhr.onerror = function() {
// Callback для обробки помилок
console.error('Помилка запиту');
};
xhr.send();
Node.js прийняв callback як основний механізм асинхронності, але запровадив чітку конвенцію для уніфікації обробки помилок — Error-First Callback pattern. Це дозволило стандартизувати інтерфейси всіх асинхронних API у екосистемі та створити передбачувану поведінку для розробників.
Важливо розуміти, що callback не є «застарілою» технологією, яку не варто вивчати. Навіть сьогодні багато бібліотек (наприклад, стрім-API Node.js, низькорівневі бібліотеки) використовують callback для максимальної гнучкості та продуктивності. Крім того, розуміння callback є необхідним для опанування промісів та async/await, оскільки ці абстракції побудовані саме поверх callback-based моделі.
Анатомія callback-функції
Callback (функція зворотного виклику) — це звичайна функція JavaScript, яка передається як аргумент іншій функції. Викликаюча функція (caller) виконує асинхронну операцію та після її завершення (успішного або з помилкою) викликає переданий callback, передаючи йому результат або інформацію про помилку.
Розглянемо найпростіший приклад callback, що не пов'язаний з асинхронністю:
function greet(name: string, callback: (message: string) => void): void {
const message = `Привіт, ${name}!`;
callback(message); // Викликаємо callback і передаємо йому результат
}
// Використання
greet('Олексій', (msg) => {
console.log(msg); // Вивід: Привіт, Олексій!
});
У цьому прикладі функція greet приймає два аргументи: рядок name та функцію callback. Після формування привітання функція викликає callback, передаючи йому сформований текст. Це ілюструє базовий принцип: callback дозволяє викликаючій функції повернути результат не через return, а через виклик переданої функції.
Тепер розглянемо асинхронний приклад із таймером:
function delayedGreeting(name: string, callback: (message: string) => void): void {
console.log('Запуск таймера...');
setTimeout(() => {
const message = `Привіт, ${name}! (з затримкою)`;
callback(message);
}, 2000); // Затримка 2 секунди
console.log('Функція delayedGreeting завершила виконання');
}
// Використання
delayedGreeting('Марія', (msg) => {
console.log(msg);
});
console.log('Основний код продовжує виконання');
Порядок виконання:
Запуск таймера...
Функція delayedGreeting завершила виконання
Основний код продовжує виконання
Привіт, Марія! (з затримкою)
Як видно, функція delayedGreeting завершує своє виконання майже миттєво (вона лише реєструє таймер), а callback викликається пізніше, коли Event Loop обробляє спрацювання таймера через 2 секунди. Це демонструє ключову властивість асинхронних callback: вони відокремлюють момент ініціації операції від моменту отримання результату.
return одразу після виконання функції. В асинхронному коді результат передається через callback у невизначений момент у майбутньому, коли операція завершиться.Error-First Callback Pattern: стандарт Node.js
Однією з найважливіших конвенцій Node.js є Error-First Callback pattern — угода про те, як повинні виглядати сигнатури callback-функцій. Згідно з цим патерном, перший параметр callback завжди зарезервований для помилки, а наступні параметри — для успішного результату операції.
Стандартна сигнатура Error-First Callback виглядає так:
type ErrorFirstCallback<T> = (error: Error | null, result?: T) => void;
Якщо операція завершилася успішно, перший параметр містить null, а результат передається у другому параметрі. Якщо сталася помилка, перший параметр містить об'єкт Error, а другий параметр може бути undefined або взагалі відсутній.
Розглянемо класичний приклад читання файлу з модуля fs:
import { readFile } from 'node:fs';
readFile('./config.json', 'utf-8', (error, data) => {
if (error) {
// Обробка помилки (файл не знайдено, немає прав доступу тощо)
console.error('Помилка читання файлу:', error.message);
return; // Важливо: припиняємо виконання callback
}
// Якщо помилки немає, обробляємо дані
try {
const config = JSON.parse(data);
console.log('Конфігурація завантажена:', config);
} catch (parseError) {
console.error('Помилка парсингу JSON:', parseError);
}
});
Чому перший параметр саме помилка? Це архітектурне рішення має кілька важливих переваг:
- Форсування обробки помилок: розробник змушений одразу перевірити наявність помилки на початку callback, що знижує ймовірність «німого» ігнорування помилок.
- Уніфікація API: усі асинхронні функції у Node.js та екосистемі npm дотримуються цієї конвенції, що робить код передбачуваним.
- Явна обробка виключень: на відміну від синхронного коду з блоками
try/catch, у асинхронному callback-based коді виключення, що виникають всередині callback, не можуть бути перехоплені зовнішнім блокомtry/catch. Error-First pattern робить обробку помилок явною.
try/catch, що огортає виклик асинхронної функції. Наприклад, цей код не спрацює:try {
readFile('./config.json', 'utf-8', (error, data) => {
throw new Error('Щось пішло не так'); // Ця помилка НЕ буде перехоплена
});
} catch (error) {
console.error('Ця гілка ніколи не виконається');
}
Правильний спосіб обробки помилок — усередині самого callback:
readFile('./config.json', 'utf-8', (error, data) => {
if (error) {
// Обробка помилки читання файлу
console.error('Помилка файлової системи:', error);
return;
}
try {
const config = JSON.parse(data);
console.log(config);
} catch (parseError) {
// Обробка помилки парсингу JSON
console.error('Помилка парсингу:', parseError);
}
});
Вкладені callback та народження Callback Hell
Реальні застосунки рідко виконують лише одну асинхронну операцію. Частіше виникає необхідність виконати кілька послідовних асинхронних дій, де кожна наступна залежить від результату попередньої. Наприклад:
- Прочитати конфігурацію з файлу.
- Підключитися до бази даних, використовуючи параметри з конфігурації.
- Виконати SQL-запит для отримання даних користувача.
- Записати отримані дані у лог-файл.
У callback-based підході кожна наступна операція розміщується всередині callback попередньої:
import { readFile, writeFile } from 'node:fs';
import { Client } from 'pg'; // PostgreSQL клієнт
readFile('./config.json', 'utf-8', (configError, configData) => {
if (configError) {
console.error('Помилка читання конфігурації:', configError);
return;
}
const config = JSON.parse(configData);
const client = new Client({
host: config.database.host,
port: config.database.port,
database: config.database.name,
});
client.connect((connectError) => {
if (connectError) {
console.error('Помилка підключення до БД:', connectError);
return;
}
client.query('SELECT * FROM users WHERE id = $1', [42], (queryError, result) => {
if (queryError) {
console.error('Помилка виконання запиту:', queryError);
client.end();
return;
}
const userData = JSON.stringify(result.rows[0], null, 2);
writeFile('./user-42.json', userData, 'utf-8', (writeError) => {
if (writeError) {
console.error('Помилка запису файлу:', writeError);
} else {
console.log('Дані користувача збережено у файл');
}
client.end(); // Закриваємо з'єднання
});
});
});
});
Цей код функціонально коректний, але має серйозні проблеми з читабельністю та підтримкою. Кожен рівень вкладеності додає індентацію, утворюючи характерну піраміду коду, яку жартома називають Callback Hell (пекло колбеків) або Pyramid of Doom (піраміда приречених).
Проблеми цього коду:
- Погана читабельність: код «дрейфує» вправо з кожною новою операцією, що ускладнює відстеження логіки виконання.
- Дублювання обробки помилок: кожен callback містить власний блок перевірки помилок, що призводить до повторюваного коду.
- Складність рефакторингу: виділення частини логіки у окрему функцію вимагає передачі контексту через додаткові параметри.
- Проблеми з потоком керування: забути
returnпісля обробки помилки — типова помилка, що призводить до виконання коду у некоректному стані.
client.end()) у разі помилки на проміжному етапі. Це призводить до витоку ресурсів (resource leak) — незакриті з'єднання накопичуються, і врешті-решт сервер вичерпує пул підключень до бази даних.Проблема «забутого return» після callback
Однією з найпідступніших помилок у callback-based коді є відсутність return після виклику callback у гілці обробки помилки. Розглянемо приклад:
import { readFile } from 'node:fs';
function loadUserData(userId: number, callback: (error: Error | null, user?: any) => void): void {
readFile(`./users/${userId}.json`, 'utf-8', (error, data) => {
if (error) {
callback(error); // ❌ ПОМИЛКА: немає return
// Виконання продовжується далі!
}
// Цей код виконається навіть у разі помилки
const user = JSON.parse(data); // TypeError: Cannot read property 'parse' of undefined
callback(null, user); // callback викликається вдруге!
});
}
У цьому коді відсутній return після виклику callback(error). Це означає, що навіть якщо сталася помилка читання файлу, виконання продовжиться далі, і спроба парсингу undefined призведе до виключення. Більш того, callback буде викликано двічі: спочатку з помилкою, потім з виключенням парсингу.
Правильний варіант:
function loadUserData(userId: number, callback: (error: Error | null, user?: any) => void): void {
readFile(`./users/${userId}.json`, 'utf-8', (error, data) => {
if (error) {
callback(error);
return; // ✅ Припиняємо виконання функції
}
try {
const user = JSON.parse(data);
callback(null, user);
} catch (parseError) {
callback(parseError as Error);
}
});
}
Або альтернативний стиль із раннім виходом (early return):
function loadUserData(userId: number, callback: (error: Error | null, user?: any) => void): void {
readFile(`./users/${userId}.json`, 'utf-8', (error, data) => {
if (error) return callback(error); // ✅ Одним рядком
try {
const user = JSON.parse(data);
callback(null, user);
} catch (parseError) {
callback(parseError as Error);
}
});
}
return callback(...) як єдиного виразу гарантує, що після виклику callback функція завершить виконання. Це усуває цілий клас помилок, пов'язаних із забутим return.Інверсія керування: втрата контролю над виконанням
Callback-based підхід має фундаментальну архітектурну проблему, яку називають інверсією керування (inversion of control). Коли ви передаєте callback у функцію, ви передаєте їй контроль над тим, коли, скільки разів і в якому контексті цей callback буде викликано.
Розглянемо приклад із гіпотетичною бібліотекою для обробки платежів:
import { processPayment } from 'third-party-payment-lib';
function checkout(userId: number, amount: number): void {
processPayment(userId, amount, (error, receipt) => {
if (error) {
console.error('Помилка платежу:', error);
return;
}
// ❌ НЕБЕЗПЕКА: ми не контролюємо, скільки разів це виконається
chargeCreditCard(userId, amount);
sendConfirmationEmail(userId, receipt);
updateInventory(receipt.items);
});
}
У цьому коді ми покладаємося на те, що бібліотека processPayment коректно викличе наш callback рівно один раз після завершення операції. Але що, якщо:
- Бібліотека містить баг і викликає callback двічі? Користувача буде списано подвійну суму.
- Callback викликається синхронно замість асинхронно? Порушується передбачувана поведінка Event Loop.
- Callback ніколи не викликається? Платіж зависає у невизначеному стані.
- Callback викликається без параметрів або з некоректними типами? Код ламається у непередбачуваний спосіб.
Проблема полягає у тому, що ми не маємо жодних гарантій щодо поведінки сторонньої бібліотеки. Усю відповідальність за коректність асинхронного потоку виконання перекладено на розробників бібліотеки, у яких можуть бути баги або інше розуміння контракту callback.
Рефакторинг Callback Hell: іменовані функції
Одним із способів пом'якшення Callback Hell є винесення вкладених callback у окремі іменовані функції. Перепишемо попередній приклад із читанням конфігурації, підключенням до бази даних та записом файлу:
import { readFile, writeFile } from 'node:fs';
import { Client } from 'pg';
function loadConfig(callback: (error: Error | null, config?: any) => void): void {
readFile('./config.json', 'utf-8', (error, data) => {
if (error) return callback(error);
try {
const config = JSON.parse(data);
callback(null, config);
} catch (parseError) {
callback(parseError as Error);
}
});
}
function connectToDatabase(config: any, callback: (error: Error | null, client?: Client) => void): void {
const client = new Client({
host: config.database.host,
port: config.database.port,
database: config.database.name,
});
client.connect((error) => {
if (error) return callback(error);
callback(null, client);
});
}
function fetchUser(client: Client, userId: number, callback: (error: Error | null, user?: any) => void): void {
client.query('SELECT * FROM users WHERE id = $1', [userId], (error, result) => {
if (error) return callback(error);
callback(null, result.rows[0]);
});
}
function saveUserToFile(user: any, callback: (error: Error | null) => void): void {
const userData = JSON.stringify(user, null, 2);
writeFile(`./user-${user.id}.json`, userData, 'utf-8', callback);
}
// Основна логіка
loadConfig((error, config) => {
if (error) {
console.error('Помилка завантаження конфігурації:', error);
return;
}
connectToDatabase(config, (error, client) => {
if (error) {
console.error('Помилка підключення до БД:', error);
return;
}
fetchUser(client!, 42, (error, user) => {
if (error) {
console.error('Помилка отримання користувача:', error);
client!.end();
return;
}
saveUserToFile(user, (error) => {
if (error) {
console.error('Помилка збереження файлу:', error);
} else {
console.log('Дані користувача збережено');
}
client!.end();
});
});
});
});
Цей підхід покращує читабельність через виділення кожної операції у окрему функцію з описовою назвою. Проте проблема глибокої вкладеності залишається — ми просто перенесли складність у структуру викликів.
async.js пропонували утиліти для композиції callback-based операцій (async.waterfall, async.series, async.parallel), але навіть вони не вирішували фундаментальних проблем читабельності та обробки помилок. З появою промісів та async/await необхідність у таких бібліотеках практично зникла.Обробка помилок у callback-based коді: обмеження та ризики
Одна з найбільших проблем callback — складність централізованої обробки помилок. У синхронному коді можна огорнути блок викликів у try/catch та обробити всі помилки в одному місці:
// Синхронний код — централізована обробка помилок
try {
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));
const client = connectToDatabase(config);
const user = fetchUser(client, 42);
saveUserToFile(user);
console.log('Успішно');
} catch (error) {
console.error('Сталася помилка:', error);
// Централізована логіка відкату (rollback), логування, сповіщення
}
У callback-based коді кожна асинхронна операція вимагає власної обробки помилок:
readFile('./config.json', 'utf-8', (error, data) => {
if (error) {
handleError(error); // Обробка помилки 1
return;
}
connectToDatabase(config, (error, client) => {
if (error) {
handleError(error); // Обробка помилки 2
return;
}
fetchUser(client, 42, (error, user) => {
if (error) {
handleError(error); // Обробка помилки 3
client.end();
return;
}
saveUserToFile(user, (error) => {
if (error) {
handleError(error); // Обробка помилки 4
}
client.end();
});
});
});
});
Це призводить до кількох проблем:
- Дублювання коду обробки помилок: кожна гілка помилки виглядає майже ідентично.
- Складність управління ресурсами: потрібно пам'ятати закривати з'єднання до бази даних у кожній гілці помилки.
- Відсутність стека викликів: якщо помилка виникає глибоко у вкладених callback, стек викликів (call stack) може не містити корисної інформації про контекст, оскільки оригінальні функції вже завершилися.
uncaughtException та за замовчуванням завершується з кодом помилки. Це критично для продакшн-серверів, де один необроблений виняток може покласти весь сервіс.Реальний приклад: HTTP API з callback-based логікою
Розглянемо типовий HTTP API endpoint, написаний у callback-стилі, який:
- Приймає POST-запит з даними користувача.
- Перевіряє наявність користувача у базі даних.
- Якщо користувач існує — повертає помилку 409 Conflict.
- Якщо не існує — створює нового користувача та повертає його дані.
import { createServer } from 'node:http';
import { Client } from 'pg';
const dbClient = new Client({
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'postgres',
password: 'password',
});
dbClient.connect((error) => {
if (error) {
console.error('Не вдалося підключитися до БД:', error);
process.exit(1);
}
});
const server = createServer((req, res) => {
if (req.method === 'POST' && req.url === '/api/users') {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
let userData;
try {
userData = JSON.parse(body);
} catch (parseError) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
return;
}
// Перевірка існування користувача
dbClient.query(
'SELECT id FROM users WHERE email = $1',
[userData.email],
(error, result) => {
if (error) {
console.error('Помилка запиту до БД:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Database error' }));
return;
}
if (result.rows.length > 0) {
// Користувач вже існує
res.writeHead(409, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User already exists' }));
return;
}
// Створення нового користувача
dbClient.query(
'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id, email, name, created_at',
[userData.email, userData.name],
(error, result) => {
if (error) {
console.error('Помилка створення користувача:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to create user' }));
return;
}
const newUser = result.rows[0];
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(newUser));
}
);
}
);
});
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Сервер слухає на порту 3000');
});
Цей код демонструє типові проблеми callback-based архітектури:
- 4 рівні вкладеності:
req.on('end')→ перша перевірка → друга перевірка → створення користувача. - Повторювана логіка відповідей: кожна гілка помилки формує HTTP-відповідь майже ідентичним чином.
- Складність тестування: для юніт-тестування потрібно мокати не лише базу даних, але й структуру подій
req.on.
async/await, яка буде розглянута у наступних лекціях. Різниця у читабельності та підтримці буде разючою — складний callback-код перетворюється у лінійну послідовність операцій.Чому індустрія відмовилася від callback на користь промісів
До 2015 року callback були стандартом де-факто для асинхронного програмування у Node.js. Проте накопичені проблеми призвели до активних пошуків альтернатив. Ключові причини переходу:
🔴 Проблеми callback
- Callback Hell: глибока вкладеність робить код важкочитабельним.
- Відсутність композиції: складно комбінувати кілька асинхронних операцій.
- Інверсія керування: втрата контролю над викликом callback.
- Дублювання обробки помилок: кожен рівень вимагає власної перевірки
if (error). - Складність паралельного виконання: для запуску кількох операцій одночасно потрібні бібліотеки на кшталт
async.js.
🟢 Переваги промісів
- Лінійний синтаксис: ланцюжки
.then()усувають вкладеність. - Централізована обробка помилок: один
.catch()для всього ланцюжка. - Контроль виконання: проміс — це об'єкт, який можна передавати, комбінувати та трансформувати.
- Вбудована підтримка паралелізму:
Promise.all(),Promise.race(). - Стандартизація: проміси є частиною ECMAScript, підтримуються нативно усіма сучасними середовищами.
У 2015 році специфікація ECMAScript 6 офіційно включила Promise у стандарт мови. Node.js почав поступово додавати Promise-based API до стандартних модулів (модуль fs/promises з'явився у версії 10.0.0 у 2018 році). З 2017 року синтаксис async/await (ECMAScript 2017) остаточно закріпив перехід індустрії від callback до сучасних асинхронних патернів.
Сучасне місце callback у Node.js екосистемі
Незважаючи на перехід індустрії до промісів та async/await, callback залишаються актуальними у певних сценаріях:
Подієві інтерфейси (EventEmitter)
Модуль events та його похідні (HTTP streams, TCP sockets, процеси) використовують callback для обробки подій:
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
// Callback для події 'data'
emitter.on('data', (chunk: Buffer) => {
console.log('Отримано дані:', chunk.toString());
});
// Callback для події 'error'
emitter.on('error', (error: Error) => {
console.error('Сталася помилка:', error);
});
emitter.emit('data', Buffer.from('Hello'));
Стрімові API (Streams)
Робота зі стрімами природно використовує callback для обробки подій:
import { createReadStream } from 'node:fs';
const stream = createReadStream('./large-file.txt', { encoding: 'utf-8' });
stream.on('data', (chunk: string) => {
console.log('Прочитано шматок:', chunk.length, 'байтів');
});
stream.on('end', () => {
console.log('Читання завершено');
});
stream.on('error', (error: Error) => {
console.error('Помилка читання:', error);
});
Низькорівневі бібліотеки
Деякі низькорівневі бібліотеки (наприклад, драйвери баз даних, нативні розширення) все ще використовують callback для максимальної продуктивності та контролю.
util.promisify або вручну. Це дозволяє використовувати сучасний синтаксис async/await навіть зі старими бібліотеками.Приклад обгортання callback у проміс:
import { readFile as readFileCallback } from 'node:fs';
import { promisify } from 'node:util';
// Автоматичне перетворення callback-based функції у Promise-based
const readFile = promisify(readFileCallback);
// Тепер можна використовувати з async/await
async function loadConfig(): Promise<any> {
const data = await readFile('./config.json', 'utf-8');
return JSON.parse(data);
}
Практичні рекомендації при роботі з callback
Якщо ви працюєте зі старим кодом або бібліотеками, що використовують callback, дотримуйтесь цих правил:
Правило 1: Завжди дотримуйтесь Error-First Convention
Перший параметр callback має бути зарезервовано для помилки, навіть якщо операція ніколи не може завершитися з помилкою. Передавайте null як перший аргумент у разі успіху.
Правило 2: Використовуйте return callback(...) для раннього виходу
Одразу після виклику callback повертайте значення, щоб запобігти подальшому виконанню коду у некоректному стані.
Правило 3: Ніколи не викликайте callback більше одного разу
Забезпечте, щоб callback викликався рівно один раз на кожен шлях виконання. Багатократний виклик призводить до непередбачуваної поведінки.
Правило 4: Обробляйте виключення всередині callback
Виключення, що виникають всередині callback, не можуть бути перехоплені зовнішнім try/catch. Огортайте код у try/catch безпосередньо у callback.
Правило 5: Розгляньте міграцію на проміси
Для нового коду завжди використовуйте проміси або async/await. Для старого коду розгляньте можливість поступової міграції через обгортання у promisify.
Порівняльна таблиця: callback vs проміси vs async/await
Для наочності розглянемо одну і ту ж операцію у різних стилях:
import { readFile } from 'node:fs';
function loadAndParse(callback: (error: Error | null, data?: any) => void): void {
readFile('./data.json', 'utf-8', (error, content) => {
if (error) return callback(error);
try {
const data = JSON.parse(content);
callback(null, data);
} catch (parseError) {
callback(parseError as Error);
}
});
}
// Використання
loadAndParse((error, data) => {
if (error) {
console.error('Помилка:', error);
return;
}
console.log('Дані:', data);
});
import { readFile } from 'node:fs/promises';
function loadAndParse(): Promise<any> {
return readFile('./data.json', 'utf-8')
.then(content => JSON.parse(content));
}
// Використання
loadAndParse()
.then(data => console.log('Дані:', data))
.catch(error => console.error('Помилка:', error));
import { readFile } from 'node:fs/promises';
async function loadAndParse(): Promise<any> {
const content = await readFile('./data.json', 'utf-8');
return JSON.parse(content);
}
// Використання
try {
const data = await loadAndParse();
console.log('Дані:', data);
} catch (error) {
console.error('Помилка:', error);
}
Підсумок та закріплення матеріалу
У цій лекції ми детально розглянули callback як перший механізм асинхронності у Node.js:
📌 Error-First Callback
- Стандарт Node.js: перший параметр — помилка, другий — результат.
- Форсує явну обробку помилок на кожному рівні.
- Не можна перехопити помилки через зовнішній
try/catch. - Вимагає
returnпісля виклику callback для запобігання подальшого виконання.
🔴 Callback Hell
- Глибока вкладеність призводить до піраміди коду.
- Дублювання логіки обробки помилок.
- Складність управління ресурсами (закриття з'єднань, файлів).
- Рішення: іменовані функції, бібліотеки
async.js(застарілі), міграція на проміси.
⚠️ Інверсія керування
- Передача callback = передача контролю над виконанням.
- Немає гарантій щодо кількості викликів, таймінгу, параметрів.
- Проміси повертають контроль розробнику через явний інтерфейс.
🔄 Сучасний контекст
- Callback залишаються у EventEmitter, Streams, низькорівневих API.
- Для нового коду використовуйте проміси та
async/await. - Старі callback-based API можна обгорнути через
util.promisify.
util.promisify — вбудована утиліта Node.js, яка автоматично перетворює функції у форматі Error-First Callback у функції, що повертають проміси. Вона працює з будь-якою функцією, остання parameter якої є callback у форматі (error, result) => void. Приклад:
import { readFile as readFileCallback } from 'node:fs';
import { promisify } from 'node:util';
const readFile = promisify(readFileCallback);
// Тепер можна використовувати з async/await
const content = await readFile('./file.txt', 'utf-8');
'error'. Розробник підписується на цю подію та обробляє помилки централізовано, а не у кожному callback окремо. Це усуває необхідність перевірки if (error) у кожному обробнику даних.У наступній лекції ми детально розглянемо проміси (Promises) — сучасний механізм асинхронності, який вирішує більшість проблем callback-based коду та є основою для async/await.