Node.js однопотоковий і використовує неблокуючий I/O. Замість створення нових потоків, Node.js реєструє callback-функції, які викликаються, коли операція завершується. Це схоже на CompletableFuture у Java, але є основним способом роботи.
1. Callbacks: Перший підхід до асинхронності
Callback — це функція, яку ви передаєте як аргумент і яка викликається після завершення операції. У Java це можна порівняти з передачею Consumer або Function як параметра.
// Node.js - callback pattern
import fs from 'fs';
// Error-first callback convention
fs.readFile('user.json', 'utf8', (err, data) => {
if (err) {
console.error('Помилка читання файлу:', err);
return;
}
console.log('Дані:', data);
});
console.log('Цей рядок виведеться ПЕРШИМ!');
Конвенція Error-First Callback: Перший параметр завжди помилка (або null), другий — результат. Це стандарт у Node.js.
Приклад: HTTP-запит з callbacks
import http from 'http';
function fetchUser(userId: number, callback: (err: Error | null, user?: any) => void) {
http.get(`http://api.example.com/users/${userId}`, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const user = JSON.parse(data);
callback(null, user);
} catch (err) {
callback(err as Error);
}
});
}).on('error', (err) => {
callback(err);
});
}
// Використання
fetchUser(123, (err, user) => {
if (err) {
console.error(err);
return;
}
console.log('User:', user);
});
Проблема: Callback Hell (Піраміда Doom)
Коли потрібно виконати кілька асинхронних операцій послідовно, код стає нечитабельним:
// Callback Hell - погана практика!
fetchUser(123, (err, user) => {
if (err) return console.error(err);
fetchOrders(user.id, (err, orders) => {
if (err) return console.error(err);
fetchOrderDetails(orders[0].id, (err, details) => {
if (err) return console.error(err);
updateInventory(details.items, (err, result) => {
if (err) return console.error(err);
sendNotification(user.email, result, (err) => {
if (err) return console.error(err);
console.log('Все готово!');
});
});
});
});
});
У Java це виглядало б як вкладені CompletableFuture з вкладеними thenAccept. Неприємно, чи не так?
2. Promises: Розв'язання проблеми Callback Hell
Promise — це об'єкт, що представляє майбутнє значення. Аналог у Java — CompletableFuture<T>.
Три стани Promise:
- Pending — очікує виконання (як незавершений Future)
- Fulfilled — успішно завершено з результатом
- Rejected — завершено з помилкою
// Створення Promise
function fetchUser(userId: number): Promise<any> {
return new Promise((resolve, reject) => {
http.get(`http://api.example.com/users/${userId}`, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const user = JSON.parse(data);
resolve(user); // Еквівалент completableFuture.complete(user)
} catch (err) {
reject(err); // Еквівалент completableFuture.completeExceptionally(err)
}
});
}).on('error', (err) => {
reject(err);
});
});
}
Ланцюжки Promise (Promise Chaining)
Метод .then() повертає новий Promise, що дозволяє будувати ланцюжки:
// Promise chaining - набагато читабельніше!
fetchUser(123)
.then(user => {
console.log('Користувач:', user);
return fetchOrders(user.id); // Повертаємо новий Promise
})
.then(orders => {
console.log('Замовлення:', orders);
return fetchOrderDetails(orders[0].id);
})
.then(details => {
console.log('Деталі:', details);
return updateInventory(details.items);
})
.then(result => {
console.log('Результат:', result);
})
.catch(err => {
// Один обробник помилок для всього ланцюжка!
console.error('Помилка:', err);
})
.finally(() => {
console.log('Завершено (незалежно від результату)');
});
Це схоже на CompletableFuture.thenCompose() у Java.
Паралельне виконання з Promise.all
// Аналог CompletableFuture.allOf() у Java
const promises = [
fetchUser(123),
fetchUser(456),
fetchUser(789)
];
Promise.all(promises)
.then(users => {
console.log('Всі користувачі:', users);
// users - це масив результатів у тому ж порядку
})
.catch(err => {
// Якщо хоча б один Promise rejected
console.error('Помилка:', err);
});
Promise.race та Promise.allSettled
// Promise.race - повертає перший завершений Promise
Promise.race([
fetchFromPrimaryDB(),
fetchFromReplicaDB()
])
.then(result => console.log('Найшвидша відповідь:', result));
// Promise.allSettled - чекає на всі Promise (навіть rejected)
Promise.allSettled([
fetchUser(123),
fetchUser(999), // Може не існувати
fetchUser(456)
])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Успіх:', result.value);
} else {
console.log('Помилка:', result.reason);
}
});
});
3. Async/Await: Синтаксичний цукор над Promises
У Java 21+ з'явилися Virtual Threads та Structured Concurrency. Async/await у JavaScript з'явився раніше і робить асинхронний код схожим на синхронний.
// Async/await - найсучасніший підхід
async function processOrder(userId: number): Promise<void> {
try {
// await "розпаковує" Promise і чекає на результат
const user = await fetchUser(userId);
console.log('Користувач:', user);
const orders = await fetchOrders(user.id);
console.log('Замовлення:', orders);
const details = await fetchOrderDetails(orders[0].id);
console.log('Деталі:', details);
const result = await updateInventory(details.items);
console.log('Результат:', result);
} catch (err) {
// Ловимо помилки з будь-якого await
console.error('Помилка:', err);
} finally {
console.log('Завершено');
}
}
// Виклик async функції
processOrder(123);
Важливо: await можна використовувати тільки всередині async функції. Async функція завжди повертає Promise.
Паралельне виконання з async/await
// НЕПРАВИЛЬНО - послідовне виконання (повільно!)
async function getMultipleUsers() {
const user1 = await fetchUser(123); // Чекаємо 1 секунду
const user2 = await fetchUser(456); // Чекаємо ще 1 секунду
const user3 = await fetchUser(789); // Чекаємо ще 1 секунду
return [user1, user2, user3]; // Загалом: 3 секунди
}
// ПРАВИЛЬНО - паралельне виконання (швидко!)
async function getMultipleUsersFast() {
// Запускаємо всі Promise одночасно
const [user1, user2, user3] = await Promise.all([
fetchUser(123),
fetchUser(456),
fetchUser(789)
]);
return [user1, user2, user3]; // Загалом: ~1 секунда
}
Порівняння з Java Spring Boot
// Java/Spring Boot - блокуючий підхід
@Service
public class OrderService {
public OrderResponse processOrder(Long userId) {
User user = userRepository.findById(userId);
List<Order> orders = orderRepository.findByUserId(userId);
OrderDetails details = orderClient.getDetails(orders.get(0).getId());
return new OrderResponse(user, orders, details);
}
}
// Node.js - async/await
async function processOrder(userId: number): Promise<OrderResponse> {
const user = await userRepository.findById(userId);
const orders = await orderRepository.findByUserId(userId);
const details = await orderClient.getDetails(orders[0].id);
return { user, orders, details };
}
// Java - з CompletableFuture (реактивний підхід)
public CompletableFuture<OrderResponse> processOrderAsync(Long userId) {
return userRepository.findByIdAsync(userId)
.thenCompose(user ->
orderRepository.findByUserIdAsync(userId)
.thenCompose(orders ->
orderClient.getDetailsAsync(orders.get(0).getId())
.thenApply(details ->
new OrderResponse(user, orders, details)
)
)
);
}
Обробка помилок: порівняння підходів
| Підхід | Обробка помилок | Читабельність |
|---|---|---|
| Callbacks | Error-first параметр, перевірка в кожному callback | ❌ Погано (callback hell) |
| Promises | .catch() в кінці ланцюжка |
✅ Добре |
| Async/Await | try/catch блоки (як у Java!) |
✅✅ Відмінно |
Типові помилки та best practices
// ❌ ПОМИЛКА 1: Забути return у Promise chain
fetchUser(123)
.then(user => {
fetchOrders(user.id); // Забули return!
// Наступний .then() отримає undefined
})
.then(orders => {
console.log(orders); // undefined!
});
// ✅ ПРАВИЛЬНО
fetchUser(123)
.then(user => {
return fetchOrders(user.id); // Повертаємо Promise
})
.then(orders => {
console.log(orders); // Масив замовлень
});
// ❌ ПОМИЛКА 2: Не обробляти rejected promises
async function riskyOperation() {
await somethingThatMightFail(); // Якщо fail, необроблена помилка!
}
// ✅ ПРАВИЛЬНО
async function safeOperation() {
try {
await somethingThatMightFail();
} catch (err) {
console.error('Обробили помилку:', err);
// Або прокинути далі: throw err;
}
}
// ❌ ПОМИЛКА 3: Змішувати callbacks і promises
async function confused() {
fs.readFile('file.txt', (err, data) => { // Callback
return data; // Це НЕ СПРАЦЮЄ!
});
}
// ✅ ПРАВИЛЬНО: Використовувати promise-based API
import { readFile } from 'fs/promises';
async function correct() {
const data = await readFile('file.txt', 'utf8');
return data;
}
Util.promisify: Перетворення callbacks у promises
import { promisify } from 'util';
import fs from 'fs';
// Перетворюємо callback-based функцію на Promise-based
const readFileAsync = promisify(fs.readFile);
async function readConfig() {
try {
const data = await readFileAsync('config.json', 'utf8');
return JSON.parse(data);
} catch (err) {
console.error('Помилка читання конфігу:', err);
throw err;
}
}
// Або використовувати вбудований fs/promises:
import { readFile } from 'fs/promises';
async function readConfigModern() {
const data = await readFile('config.json', 'utf8');
return JSON.parse(data);
}
Практичний приклад: Express endpoint
import express, { Request, Response } from 'express';
const app = express();
// ПОГАНО: callback hell в Express
app.get('/user/:id/orders', (req, res) => {
getUserById(req.params.id, (err, user) => {
if (err) return res.status(500).json({ error: err.message });
getOrdersByUserId(user.id, (err, orders) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ user, orders });
});
});
});
// ДОБРЕ: async/await (як контролер у Spring!)
app.get('/user/:id/orders', async (req: Request, res: Response) => {
try {
const user = await getUserById(req.params.id);
const orders = await getOrdersByUserId(user.id);
res.json({ user, orders });
} catch (err) {
res.status(500).json({
error: err instanceof Error ? err.message : 'Unknown error'
});
}
});
// ЩЕ КРАЩЕ: з обробником помилок
app.get('/user/:id/orders', asyncHandler(async (req: Request, res: Response) => {
const user = await getUserById(req.params.id);
const orders = await getOrdersByUserId(user.id);
res.json({ user, orders });
}));
// Utility для автоматичної обробки помилок
function asyncHandler(fn: Function) {
return (req: Request, res: Response, next: Function) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
Підсумок: Що використовувати?
- Callbacks — уникайте в новому коді. Використовуйте тільки якщо працюєте зі старими бібліотеками.
- Promises — добре для складних ланцюжків та комбінування операцій (Promise.all, Promise.race).
- Async/Await — основний вибір для більшості випадків. Код виглядає як синхронний Java-код.
Якщо ви з Java/Spring Boot світу — використовуйте async/await. Це найближче до того, як ви пишете код зараз, але з перевагами неблокуючого I/O.
Коментарі
Дописати коментар