Promisify, callbackify, asyncify

Адаптеры между тремя контрактами асинхронных функций в JavaScript: sync, callback-last (error-first) и Promise-based. Позволяют связать разные стили в одной кодовой базе.

Что это / Зачем

В Node.js и JS исторически три контракта: синхронные функции, callback-last (error-first) и Promise/async. Адаптеры превращают один в другой:

  • promisify — callback API → Promise (util.promisify встроен в Node, есть с самого начала)
  • callbackify — Promise/async → callback API (обратная совместимость с legacy)
  • asyncify — синхронная функция → асинхронная (с callback)

API / Синтаксис

// promisify — ручная реализация
const promisify = (fn) => (...args) =>
  new Promise((resolve, reject) =>
    fn(...args, (err, res) => (err ? reject(err) : resolve(res))));

const readFile = promisify(fs.readFile);
const data = await readFile('file.txt', 'utf8');

// callbackify — обратная сторона
const callbackify = (asyncFn) => (...args) => {
  const callback = args.pop();
  asyncFn(...args).then(
    (res) => callback(null, res),
    (err) => callback(err),
  );
};

// asyncify — sync функция в callback-стиль
const asyncify = (fn) => (...args) => {
  const callback = args.pop();
  setTimeout(() => {
    try { callback(null, fn(...args)); }
    catch (err) { callback(err); }
  }, 0);
};

// Встроено в Node
const { promisify } = require('node:util');

Ключевые моменты

  • Error-first callback: callback(null, result) успех, callback(err) ошибка.
  • Паттерн "две стрелочные функции" ((fn) => (...args) => ...) V8 распознаёт и хорошо оптимизирует — внешняя функция фактически нужна для сохранения аргумента в замыкании.
  • Длинное каррирование (3-4 функции в цепочке) V8 оптимизирует хуже.
  • util.promisify в Node имеет более сложную реализацию, чем учебная.
  • Адаптер может выполнять обёрнутую функцию синхронно ИЛИ асинхронно — это решение автора адаптера.

Подводные камни

  • args.pop() мутирует массив — приводит к перевыделению памяти, ломает оптимизацию. Лучше args.at(-1) + args.slice(0, -1).
  • arguments (старый объект) ломает оптимизацию всей функции — после появления rest-оператора (...args) использовать arguments нельзя.
  • setTimeout — не часть языка, а host API (в браузере и Node реализованы по-разному). В Node под капотом enroll/unenroll, реальных таймеров ОС на порядок меньше.
  • try/catch с 2014+ оптимизируется отлично (после перехода V8 с Crankshaft на Ignition+TurboFan). Можно "обвешаться try/catch".

🎓 Источники

  • ⚡ [Асинхронное программирование и оптимизация для V8 asyncify, promisify, callbackify] · 2025-12-20 · YouTube
    • Тезисы:
      • V8 спец-оптимизирует паттерн из двух стрелочных функций — кэширует вложенную функцию вместе с шаблоном контекста.
      • Самый быстрый JS близок к C — меньше динамики, лучше оптимизация.
      • args.pop() — главная проблема в типичной реализации adapter'а.
      • setTimeout — host API, в Node стоит дешевле благодаря enroll/unenroll.
      • try/catch/finally больше не вызывают деоптимизации.
      • На байт-коде try/catch — это набор if-ов (jump'ов CPU).
    • Цитата:

      «Наиболее эффективный, наиболее быстрый код в JavaScript получается тот, который ближе всего к коду, написанному на языке C. Чем больше мы избавляемся от динамичности, тем проще V8 оптимизировать этот код.»

    • Цитата:

      «setTimeout — это внешний API, которая реализована совершенно по-разному в браузере и в ноде. То есть в JavaScript вообще setTimeout нет.»

  • 🎓 [Асинхронные адаптеры promisify, callbackify, asyncify...] · 2018-12-18 · YouTube
  • 🎓 [Архив 2018 - Часть 15 адаптеры callbackify, promisify, асинхронная очередь] · 2020-01-13 · YouTube
  • 🎓 [Асинхронное программирование callbackify, class adapter, async iterator] · 2025-12-23 · YouTube

См. также