Async Adapters — promisify / callbackify / asyncify
Адаптеры между контрактами асинхронности в JS: callback-last/error-first ↔ Promise ↔ async function ↔ sync. Частный случай Adapter Pattern.
Проблема
В одном приложении сосуществуют три-четыре контракта:
- sync:
const x = fn(a, b) - callback-last, error-first:
fn(a, b, (err, res) => ...) - Promise:
fn(a, b).then(res => ...).catch(err => ...) - async:
try { const x = await fn(a, b) } catch (e) { ... }
Между ними нужно стыковать абстракции.
Решение
Унификация через адаптеры:
promisify(fn)— callback-функция → возвращает Promise (дляawait).callbackify(asyncFn)— async-функция → принимает callback (для legacy API).asyncify(syncFn)— sync-функция → callback (черезsetTimeoutилиsetImmediate).- Sync ← async — невозможно (фундаментальное ограничение).
Пример в JS
// promisify (упрощённая версия из util)
const promisify = (fn) => (...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, res) => err ? reject(err) : resolve(res));
});
const readFile = promisify(require('fs').readFile);
const data = await readFile('config.json', 'utf8');
// callbackify
const callbackify = (asyncFn) => (...args) => {
const callback = args.pop();
asyncFn(...args).then((res) => callback(null, res), (err) => callback(err));
};
// asyncify — sync → async через setTimeout
const asyncify = (fn) => (...args) => {
const callback = args.pop();
setTimeout(() => {
try {
const res = fn(...args);
callback(null, res);
} catch (err) {
callback(err);
}
}, 0);
};
Где используется в JS-экосистеме
util.promisify(Node.js) — встроенныйutil.callbackify(Node.js) — встроенныйbluebird.promisifyAll— для promisify целой библиотеки- fs/promises — уже promisified версии fs
- обёртки над legacy API: SQLite3, leveldown — promisify нужен
Подводные камни
- Async → sync нельзя: никаким адаптером Promise не превращается в sync-значение.
- promisify предполагает контракт
callback-last, error-first— если другой порядок, нужен кастомный. setTimeout(fn, 0)вasyncifyотдаёт управление в event loop — гарантирует асинхронность.- promisify теряет
this— для методов нужен.bind(obj). util.promisify[util.promisify.custom]— кастомный promisifier для нестандартных контрактов.
Главные тезисы автора
- «Все эти контракты в приложениях часто используются — несколько из них одновременно».
promisifyв Node.js лежит вutil, в браузере его нет — нужно писать самому.- «Sync → async можно, async → sync нельзя» — фундаментальное ограничение.
asyncifyчерез setTimeout разрывает синхронность — отдаёт управление в event loop.- Promisify/callbackify — классические адаптеры контрактов асинхронности.
- Стыковка
callback-last, error-firstиPromise— основное назначение. - Адаптер часто использует другие паттерны внутри (revealing constructor).
🎓 Источники
- 🎓 Асинхронные адаптеры promisify, callbackify, asyncify · 2018-12-18
- Sync → async можно, async → sync нельзя
- asyncify через setTimeout
- Два замкнутых контекста
- twice/half как тестовый приём
- 🎓 Архив 2018: callbackify, promisify, асинхронная очередь · 2020-01-13
- 🎓 Асинхронное программирование callbackify, class adapter, async iterator · 2025-12-23
- 🎓 GoF Patterns Adapter for JavaScript · 2024-11-11
- promisify/callbackify как частные адаптеры