← JavaScript/Промисификация#126 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Промисификация

Вы работаете с 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 на промисы без переписывания библиотеки.

На основе предыдущих уроков

  • Promise: базовый синтаксис new Promise(resolve, reject)
  • async/await: используется после промисификации
  • колбэки: паттерн error-first callback
  • rest-параметры ...args: нужны в реализации promisify
  • Своя функция promisify

    function 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)                   // 'Алиса'
    }

    util.promisify в Node.js

    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 секунда

    В реальных проектах

  • Node.js-бэкенд: промисифицируют старые callback-API (redis, mysql2 без Promise-режима, legacy-библиотеки)
  • Тесты: оборачивают callback-based методы для использования с async/await в Jest/Mocha
  • Миграция: постепенно переводят старые модули на промисы без полного переписывания
  • sleep(): самая частая ручная промисификация — (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 на промисы без переписывания библиотеки.

    На основе предыдущих уроков

  • Promise: базовый синтаксис new Promise(resolve, reject)
  • async/await: используется после промисификации
  • колбэки: паттерн error-first callback
  • rest-параметры ...args: нужны в реализации promisify
  • Своя функция promisify

    function 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)                   // 'Алиса'
    }

    util.promisify в Node.js

    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 секунда

    В реальных проектах

  • Node.js-бэкенд: промисифицируют старые callback-API (redis, mysql2 без Promise-режима, legacy-библиотеки)
  • Тесты: оборачивают callback-based методы для использования с async/await в Jest/Mocha
  • Миграция: постепенно переводят старые модули на промисы без полного переписывания
  • sleep(): самая частая ручная промисификация — (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))

    Загружаем среду выполнения...
    Загружаем AI-помощника...