|
| 1 | +# Promisification |
| 2 | + |
| 3 | +Promisification -- is a long word for a simple transform. It's conversion of a function that accepts a callback into a function returning a promise. |
| 4 | + |
| 5 | +In other words, we create a wrapper-function that does the same, internally calling the original one, but returns a promise. |
| 6 | + |
| 7 | +Such transforms are often needed in real-life, as many functions and libraries are callback-based. But promises are more convenient. So it makes sense to promisify those. |
| 8 | + |
| 9 | +For instance, we have `loadScript(src, callback)` from the chapter <info:callbacks>. |
| 10 | + |
| 11 | +```js run |
| 12 | +function loadScript(src, callback) { |
| 13 | + let script = document.createElement('script'); |
| 14 | + script.src = src; |
| 15 | + |
| 16 | + script.onload = () => callback(null, script); |
| 17 | + script.onerror = () => callback(new Error(`Script load error for ${src}`)); |
| 18 | + |
| 19 | + document.head.append(script); |
| 20 | +} |
| 21 | + |
| 22 | +// usage: |
| 23 | +// loadScript('path/script.js', (err, script) => {...}) |
| 24 | +``` |
| 25 | + |
| 26 | +Let's promisify it. The new `loadScriptPromise(src)` function will do the same, but accept only `src` (no callback) and return a promise. |
| 27 | + |
| 28 | +```js |
| 29 | +let loadScriptPromise = function(src) { |
| 30 | + return new Promise((resolve, reject) => { |
| 31 | + loadScript(src, (err, script) => { |
| 32 | + if (err) reject(err) |
| 33 | + elsee resolve(script); |
| 34 | + }); |
| 35 | + }) |
| 36 | +} |
| 37 | + |
| 38 | +// usage: |
| 39 | +// loadScriptPromise('path/script.js').then(...) |
| 40 | +``` |
| 41 | + |
| 42 | +Now `loadScriptPromise` fits well in our promise-based code. |
| 43 | + |
| 44 | +As we can see, it delegates all the work to the original `loadScript`, providing its own callback that translates to promise `resolve/reject`. |
| 45 | + |
| 46 | +As we may need to promisify many functions, it makes sense to use a helper. |
| 47 | + |
| 48 | +That's actually very simple -- `promisify(f)` below takes a to-promisify function `f` and returns a wrapper function. |
| 49 | + |
| 50 | +That wrapper does the same as in the code above: returns a promise and passes the call to the original `f`, tracking the result in a custom callback: |
| 51 | + |
| 52 | +```js |
| 53 | +function promisify(f) { |
| 54 | + return function (...args) { // return a wrapper-function |
| 55 | + return new Promise((resolve, reject) => { |
| 56 | + function callback(err, result) { // our custom callback for f |
| 57 | + if (err) { |
| 58 | + return reject(err); |
| 59 | + } else { |
| 60 | + resolve(result); |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + args.push(callback); // append our custom callback to the end of arguments |
| 65 | + |
| 66 | + f.call(this, ...args); // call the original function |
| 67 | + }); |
| 68 | + }; |
| 69 | +}; |
| 70 | + |
| 71 | +// usage: |
| 72 | +let loadScriptPromise = promisify(loadScript); |
| 73 | +loadScriptPromise(...).then(...); |
| 74 | +``` |
| 75 | + |
| 76 | +Here we assume that the original function expects a callback with two arguments `(err, result)`. That's what we meet most often. Then our custom callbacks is exactly in the right format, and `promisify` works great for such case. |
| 77 | + |
| 78 | +But what is the original `f` expects a callback with more arguments `callback(err, res1, res2)`? |
| 79 | + |
| 80 | +Here's a modification of `promisify` that returns an array of multiple callback results: |
| 81 | + |
| 82 | +```js |
| 83 | +// promisify(f, true) to get array of results |
| 84 | +function promisify(f, manyArgs = false) { |
| 85 | + return function (...args) { |
| 86 | + return new Promise((resolve, reject) => { |
| 87 | + function *!*callback(err, ...results*/!*) { // our custom callback for f |
| 88 | + if (err) { |
| 89 | + return reject(err); |
| 90 | + } else { |
| 91 | + // resolve with all callback results if manyArgs is specified |
| 92 | + *!*resolve(manyArgs ? results : results[0]);*/!* |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + args.push(callback); |
| 97 | + |
| 98 | + f.call(this, ...args); |
| 99 | + }); |
| 100 | + }; |
| 101 | +}; |
| 102 | + |
| 103 | +// usage: |
| 104 | +f = promisify(f, true); |
| 105 | +f(...).then(err => ..., arrayOfResults => ...) |
| 106 | +``` |
| 107 | +
|
| 108 | +In some cases, `err` may be absent at all: `callback(result)`, or there's something exotic in the callback format, then we can promisify such functions manually. |
| 109 | +
|
| 110 | +There are also modules with a bit more flexible promisification functions, e.g. [es6-promisify](https://github.com/digitaldesignlabs/es6-promisify). In Node.js, there's a built-in `util.promisify` function for that. |
| 111 | +
|
| 112 | +```smart |
| 113 | +Promisification is a great approach, especially when you use `async/await` (see the next chapter), but not a total replacement for callbacks. |
| 114 | + |
| 115 | +Remember, a promise may have only one result, but a callback may technically be called many times. |
| 116 | + |
| 117 | +So promisification is only meant for functions that call the callback once. Furhter calls will be ignored. |
| 118 | +``` |
0 commit comments