Вы работаете с Node.js-модулем для чтения файлов — он принимает колбэк. Но весь остальной код уже на async/await. Приходится либо смешивать стили, либо промисифицировать — превратить функцию с колбэком в функцию, возвращающую Promise.
Библиотеки, написанные до появления промисов, используют error-first callback: первый аргумент — ошибка, второй — результат. Это соглашение Node.js:
fs.readFile('./config.json', 'utf8', (err, data) => {
if (err) {
console.error('Ошибка:', err.message)
return
}
console.log('Данные:', JSON.parse(data))
})Вложенные колбэки — "callback hell". Промисификация переводит такой API на промисы без переписывания библиотеки.
new Promise(resolve, reject)...args: нужны в реализации promisifyfunction promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
// Добавляем error-first колбэк в конец аргументов
fn(...args, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
}Функция принимает fn с колбэком и возвращает новую функцию, которая:
1. Принимает те же аргументы (без колбэка)
2. Сама добавляет колбэк в конец
3. Возвращает Promise
// До промисификации
function getUserById(id, callback) {
setTimeout(() => {
if (id <= 0) return callback(new Error('Некорректный ID'))
callback(null, { id, name: 'Алиса', role: 'admin' })
}, 100)
}
// После
const getUser = promisify(getUserById)
async function loadProfile(userId) {
const user = await getUser(userId) // вместо вложенного колбэка
console.log(user.name) // 'Алиса'
}Node.js поставляет встроенную реализацию:
const { promisify } = require('util')
const fs = require('fs')
const readFile = promisify(fs.readFile)
async function loadConfig() {
const data = await readFile('./config.json', 'utf8')
return JSON.parse(data)
}Многие Node.js API уже имеют ready-made промис-версии: fs.promises.readFile, dns.promises.lookup.
Промисификация подходит только для однократных событий. Для многократных — потеряете все события кроме первого:
// НЕ промисифицируй события, которые приходят много раз
// const onMessage = promisify(ws.on.bind(ws, 'message')) // НЕПРАВИЛЬНО
// Правильно — обычный колбэк или async-итератор
ws.on('message', (data) => {
console.log('Получено:', data) // вызывается для каждого сообщения
})Не промисифицируй: EventEmitter, WebSocket, setInterval.
Ошибка 1: забыли пробросить все аргументы
// Сломано: fn вызывается без оригинальных аргументов
function promisifyBroken(fn) {
return function() { // нет ...args
return new Promise((resolve, reject) => {
fn((err, result) => { // fn не получает аргументы!
if (err) reject(err)
else resolve(result)
})
})
}
}
// Исправлено:
function promisify(fn) {
return function(...args) { // собираем аргументы
return new Promise((resolve, reject) => {
fn(...args, (err, result) => { // передаём их в fn
if (err) reject(err)
else resolve(result)
})
})
}
}Ошибка 2: промисификация функций с несколькими результатами в колбэке
// Некоторые API передают несколько значений: callback(err, data, meta)
// Стандартный promisify вернёт только data!
// Решение: собрать все аргументы
function promisifyMulti(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, ...results) => { // rest для всех результатов
if (err) reject(err)
else resolve(results.length === 1 ? results[0] : results)
})
})
}
}Ошибка 3: промисификация setTimeout (не error-first)
// setTimeout не следует error-first, promisify не подходит
const sleep = promisify(setTimeout) // НЕ РАБОТАЕТ корректно
// Правильно — реализуем sleep вручную
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
await sleep(1000) // пауза 1 секунда(ms) => new Promise(r => setTimeout(r, ms))Реализация promisify и применение к моку базы данных
// Реализация promisify для error-first callbacks
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
}
// --- Мок базы данных с колбэками (legacy API) ---
const mockDB = {
users: [
{ id: 1, name: 'Алиса Ковалёва', role: 'admin' },
{ id: 2, name: 'Борис Смирнов', role: 'editor' },
],
orders: [
{ id: 101, userId: 1, total: 4500, status: 'delivered' },
{ id: 102, userId: 2, total: 1200, status: 'pending' },
{ id: 103, userId: 1, total: 8900, status: 'delivered' },
],
}
function findUser(userId, callback) {
setTimeout(() => {
if (userId <= 0) return callback(new Error(`Некорректный ID: ${userId}`))
const user = mockDB.users.find(u => u.id === userId)
if (!user) return callback(new Error(`Пользователь ${userId} не найден`))
callback(null, user)
}, 50)
}
function getOrdersByUser(userId, callback) {
setTimeout(() => {
const orders = mockDB.orders.filter(o => o.userId === userId)
callback(null, orders)
}, 50)
}
// Промисифицируем
const findUserAsync = promisify(findUser)
const getOrdersAsync = promisify(getOrdersByUser)
// Теперь можно использовать async/await вместо вложенных колбэков
async function getUserSummary(userId) {
try {
const user = await findUserAsync(userId)
console.log(`Пользователь: ${user.name} (${user.role})`)
// Пользователь: Алиса Ковалёва (admin)
const orders = await getOrdersAsync(userId)
const total = orders.reduce((sum, o) => sum + o.total, 0)
console.log(`Заказов: ${orders.length}, суммарно: ${total.toLocaleString('ru-RU')} ₽`)
// Заказов: 2, суммарно: 13 400 ₽
const delivered = orders.filter(o => o.status === 'delivered')
console.log(`Доставлено: ${delivered.length} из ${orders.length}`)
// Доставлено: 2 из 2
} catch (err) {
console.error('Ошибка:', err.message)
}
}
async function main() {
await getUserSummary(1)
console.log('---')
// Ошибка: пользователь не существует
try {
await findUserAsync(99)
} catch (err) {
console.error('Поймана ошибка:', err.message)
// Поймана ошибка: Пользователь 99 не найден
}
}
main()
// sleep — ручная промисификация (setTimeout не error-first)
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
async function retryWithDelay(fn, attempts, delayMs) {
for (let i = 1; i <= attempts; i++) {
try {
return await fn()
} catch (err) {
if (i === attempts) throw err
console.log(`Попытка ${i} неудачна, повтор через ${delayMs}мс...`)
await sleep(delayMs)
}
}
}Вы работаете с Node.js-модулем для чтения файлов — он принимает колбэк. Но весь остальной код уже на async/await. Приходится либо смешивать стили, либо промисифицировать — превратить функцию с колбэком в функцию, возвращающую Promise.
Библиотеки, написанные до появления промисов, используют error-first callback: первый аргумент — ошибка, второй — результат. Это соглашение Node.js:
fs.readFile('./config.json', 'utf8', (err, data) => {
if (err) {
console.error('Ошибка:', err.message)
return
}
console.log('Данные:', JSON.parse(data))
})Вложенные колбэки — "callback hell". Промисификация переводит такой API на промисы без переписывания библиотеки.
new Promise(resolve, reject)...args: нужны в реализации promisifyfunction promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
// Добавляем error-first колбэк в конец аргументов
fn(...args, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
}Функция принимает fn с колбэком и возвращает новую функцию, которая:
1. Принимает те же аргументы (без колбэка)
2. Сама добавляет колбэк в конец
3. Возвращает Promise
// До промисификации
function getUserById(id, callback) {
setTimeout(() => {
if (id <= 0) return callback(new Error('Некорректный ID'))
callback(null, { id, name: 'Алиса', role: 'admin' })
}, 100)
}
// После
const getUser = promisify(getUserById)
async function loadProfile(userId) {
const user = await getUser(userId) // вместо вложенного колбэка
console.log(user.name) // 'Алиса'
}Node.js поставляет встроенную реализацию:
const { promisify } = require('util')
const fs = require('fs')
const readFile = promisify(fs.readFile)
async function loadConfig() {
const data = await readFile('./config.json', 'utf8')
return JSON.parse(data)
}Многие Node.js API уже имеют ready-made промис-версии: fs.promises.readFile, dns.promises.lookup.
Промисификация подходит только для однократных событий. Для многократных — потеряете все события кроме первого:
// НЕ промисифицируй события, которые приходят много раз
// const onMessage = promisify(ws.on.bind(ws, 'message')) // НЕПРАВИЛЬНО
// Правильно — обычный колбэк или async-итератор
ws.on('message', (data) => {
console.log('Получено:', data) // вызывается для каждого сообщения
})Не промисифицируй: EventEmitter, WebSocket, setInterval.
Ошибка 1: забыли пробросить все аргументы
// Сломано: fn вызывается без оригинальных аргументов
function promisifyBroken(fn) {
return function() { // нет ...args
return new Promise((resolve, reject) => {
fn((err, result) => { // fn не получает аргументы!
if (err) reject(err)
else resolve(result)
})
})
}
}
// Исправлено:
function promisify(fn) {
return function(...args) { // собираем аргументы
return new Promise((resolve, reject) => {
fn(...args, (err, result) => { // передаём их в fn
if (err) reject(err)
else resolve(result)
})
})
}
}Ошибка 2: промисификация функций с несколькими результатами в колбэке
// Некоторые API передают несколько значений: callback(err, data, meta)
// Стандартный promisify вернёт только data!
// Решение: собрать все аргументы
function promisifyMulti(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, ...results) => { // rest для всех результатов
if (err) reject(err)
else resolve(results.length === 1 ? results[0] : results)
})
})
}
}Ошибка 3: промисификация setTimeout (не error-first)
// setTimeout не следует error-first, promisify не подходит
const sleep = promisify(setTimeout) // НЕ РАБОТАЕТ корректно
// Правильно — реализуем sleep вручную
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
await sleep(1000) // пауза 1 секунда(ms) => new Promise(r => setTimeout(r, ms))Реализация promisify и применение к моку базы данных
// Реализация promisify для error-first callbacks
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
}
}
// --- Мок базы данных с колбэками (legacy API) ---
const mockDB = {
users: [
{ id: 1, name: 'Алиса Ковалёва', role: 'admin' },
{ id: 2, name: 'Борис Смирнов', role: 'editor' },
],
orders: [
{ id: 101, userId: 1, total: 4500, status: 'delivered' },
{ id: 102, userId: 2, total: 1200, status: 'pending' },
{ id: 103, userId: 1, total: 8900, status: 'delivered' },
],
}
function findUser(userId, callback) {
setTimeout(() => {
if (userId <= 0) return callback(new Error(`Некорректный ID: ${userId}`))
const user = mockDB.users.find(u => u.id === userId)
if (!user) return callback(new Error(`Пользователь ${userId} не найден`))
callback(null, user)
}, 50)
}
function getOrdersByUser(userId, callback) {
setTimeout(() => {
const orders = mockDB.orders.filter(o => o.userId === userId)
callback(null, orders)
}, 50)
}
// Промисифицируем
const findUserAsync = promisify(findUser)
const getOrdersAsync = promisify(getOrdersByUser)
// Теперь можно использовать async/await вместо вложенных колбэков
async function getUserSummary(userId) {
try {
const user = await findUserAsync(userId)
console.log(`Пользователь: ${user.name} (${user.role})`)
// Пользователь: Алиса Ковалёва (admin)
const orders = await getOrdersAsync(userId)
const total = orders.reduce((sum, o) => sum + o.total, 0)
console.log(`Заказов: ${orders.length}, суммарно: ${total.toLocaleString('ru-RU')} ₽`)
// Заказов: 2, суммарно: 13 400 ₽
const delivered = orders.filter(o => o.status === 'delivered')
console.log(`Доставлено: ${delivered.length} из ${orders.length}`)
// Доставлено: 2 из 2
} catch (err) {
console.error('Ошибка:', err.message)
}
}
async function main() {
await getUserSummary(1)
console.log('---')
// Ошибка: пользователь не существует
try {
await findUserAsync(99)
} catch (err) {
console.error('Поймана ошибка:', err.message)
// Поймана ошибка: Пользователь 99 не найден
}
}
main()
// sleep — ручная промисификация (setTimeout не error-first)
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
async function retryWithDelay(fn, attempts, delayMs) {
for (let i = 1; i <= attempts; i++) {
try {
return await fn()
} catch (err) {
if (i === attempts) throw err
console.log(`Попытка ${i} неудачна, повтор через ${delayMs}мс...`)
await sleep(delayMs)
}
}
}Реализуй функцию promisify(fn), которая оборачивает функцию с error-first callback в промис. Проверь её на моковой функции fetchUserData(userId, callback) — она возвращает данные пользователя или ошибку при userId <= 0.
fn(...args, (err, result) => err ? reject(err) : resolve(result))