Skip to main content

Що таке promisification?

Promisification - це перетворення функції з колбек-стилем на функцію, яка повертає Promise. Замість вкладених колбеків отримуєш .then(), .catch() або async/await.

Теорія

TL;DR

  • Колбеки - це як записка другу: "зателефонуй, коли зробиш." Promisification загортає це в запит з чітким результатом: або успіх, або помилка.
  • Головна різниця: замість (err, result) => {} отримуєш .then(result => {}).catch(err => {}).
  • Використовуй для старих Node.js API (fs.readFile, crypto) і сторонніх бібліотек на колбеках. Якщо API вже повертає Promise - не потрібно.
  • util.promisify автоматично розпізнає стандартний Node.js патерн (err, value).

Швидкий приклад

javascript
const fs = require('fs'); const util = require('util'); // Оригінальний колбек-стиль fs.readFile('data.txt', 'utf8', (err, data) => { if (err) return console.error(err); console.log(data); // Output: вміст файлу }); // Promisified - той самий результат, без вкладеності const readFile = util.promisify(fs.readFile); const data = await readFile('data.txt', 'utf8'); console.log(data); // Output: вміст файлу

Результат однаковий. Promisified-версія дозволяє використати await і обробити помилки одним try/catch.

Ключова різниця

Стандартний колбек-патерн у Node.js очікує (err, result) останнім аргументом. Promisification загортає таку функцію в нову, яка створює Promise всередині, передає згенерований колбек оригіналу і або викликає resolve(result), або reject(err). Пишеш обгортання один раз і використовуєш скрізь.

Коли застосовувати

  • Вбудовані Node.js API (fs, crypto, dns) до появи Promises: promisify і переходь на async/await.
  • Сторонні бібліотеки з колбек-стилем: огорни один раз, використовуй ланцюжком скрізь.
  • Легасі-код, який не можна переписати: promisify інтерфейс, залиш внутрішню логіку без змін.
  • API вже повертає Promise: пропускай.

Як це працює всередині

util.promisify (доданий у Node.js v8.0.0) перевіряє, чи є у функції символ util.promisify.custom. Якщо ні - вважає, що остання функція в аргументах це колбек формату (err, value). Повертає обгортку, яка створює new Promise, викликає оригінальну функцію зі згенерованим колбеком і або resolve(value), або reject(err).

Типові помилки

Помилка: promisify функції, яка не відповідає Node.js error-first стилю.

javascript
const sleep = util.promisify(setTimeout); // TypeError: callback is not a function

setTimeout приймає простий колбек, не (err, value). Замість цього: new Promise(resolve => setTimeout(resolve, 1000)).

Помилка: втрата this при promisify методів класу.

javascript
class DB { read(id, cb) { cb(null, 'data'); } } const db = new DB(); const read = util.promisify(db.read); // this = undefined всередині read

Виправлення: util.promisify(db.read.bind(db)). Прив'язуй екземпляр перед огортанням. Ця помилка частіше призводить до непомітних збоїв у продакшені, ніж неправильна сигнатура колбека.

Помилка: очікування одного значення, коли оригінал передає кілька.

javascript
const lookup = util.promisify(dns.lookup); lookup('example.com').then(address => console.log(address)); // неправильно - address це [address, family]

dns.lookup викликає колбек з (err, address, family). util.promisify резолвить у масив [address, family]. Деструктуруй: .then(([address, family]) => ...).

Де зустрічається на практиці

  • Express: util.promisify(fs.access) перед віддачею статичних файлів.
  • MongoDB callback driver: util.promisify(collection.findOne) у старих Node.js застосунках і Lambda-функціях.
  • AWS SDK v2: util.promisify(s3.getObject) для операцій з S3.
  • Внутрішнє легасі: огортаєш один раз на межі модуля, внутрішня логіка залишається без змін.

Питання на співбесіді

Q: Яка різниця між util.promisify і ручним загортанням через new Promise?
A: util.promisify автоматично розпізнає патерн (err, value) і враховує util.promisify.custom. Ручне new Promise дає повний контроль, але колбек-логіку пишеш сам. Для стандартних Node.js API util.promisify коротший і менш схильний до помилок.

Q: Що відбувається, якщо оригінальна функція передає кілька значень у колбек?
A: util.promisify резолвить у масив усіх аргументів після err. Наприклад, dns.lookup дає [address, family]. Деструктуруй, щоб отримати кожне значення окремо.

Q: Чи можна зробити promisify синхронної функції?
A: Технічно так, але сенсу немає. Promise резолвиться миттєво, а ти додаєш зайві накладні витрати.

Q: Чому не варто використовувати util.promisify зі стрімами?
A: Стріми потребують обробки зворотного тиску (backpressure). Promisify резолвить один раз і виходить, ігноруючи потік даних. Це може призвести до витоку пам'яті. Використовуй stream.pipeline або async-ітератори.

Приклади

Callback hell проти promisification

Читання двох файлів послідовно через колбеки перетворюється на піраміду. Promisification тримає код плоским.

javascript
const fs = require('fs'); const util = require('util'); // Колбек-версія - вкладеність зростає з кожним послідовним викликом fs.readFile('data.txt', 'utf8', (err, data1) => { if (err) return console.error(err); fs.readFile('data2.txt', 'utf8', (err, data2) => { if (err) return console.error(err); console.log(data1 + data2); // Output: вміст обох файлів }); }); // Promisified - той самий результат, читається зверху вниз const readFile = util.promisify(fs.readFile); try { const data1 = await readFile('data.txt', 'utf8'); const data2 = await readFile('data2.txt', 'utf8'); console.log(data1 + data2); // Output: вміст обох файлів } catch (err) { console.error(err); }

Логіка і результат однакові. Кожен послідовний крок - один рядок, а не один рівень вкладеності.

Express-роут з promisified читанням файлів

Реальний патерн: читання конфігу користувача з диску перед відповіддю API.

javascript
const fs = require('fs'); const util = require('util'); const readFile = util.promisify(fs.readFile); app.get('/user/:id', async (req, res) => { try { const userData = await readFile(`users/${req.params.id}.json`, 'utf8'); const settings = await readFile('app-settings.json', 'utf8'); res.json({ user: JSON.parse(userData), // Output: об'єкт користувача settings: JSON.parse(settings) // Output: об'єкт налаштувань }); } catch (err) { res.status(500).json({ error: err.message }); // Output: { error: 'ENOENT...' } } });

Один try/catch покриває обидва читання. Без promisification це два вкладених колбеки з окремими гілками помилок у кожному.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?