Promises made a bit easier
I’ve noticed that some developers, especially the ones without an extensive Javascript (JS) background, can struggle a bit with understanding promises. I decided to do a write-up that explains what they are, how they work, how you can use them, and some pitfalls and tricks.
What’s a promise?
A promise represents the outcome of a certain asynchronous operation, even when that operation has not yet finished. It is quite literally a promise that once the operation is done, a function will be called with the outcome. That outcome can either be a success (which means the promise will be resolved), or it can be a failure (which means the promise will be rejected).
Each of those outcomes will call a different function, that you pass to .then()
or .catch()
:
// Method #1:
myAsynchronousFunction().then(successHandler, errorHandler)
// Method #2:
myAsynchronousFunction().then(successHandler).catch(errorHandler)
There are subtle differences between these two notations. Practically speaking,“Method #2” is preferred.
What’s the difference between promises and callbacks?
Before promises, Node.js relied on using callback functions to handle the results of asynchronous operations. Semantically, they work similar to promises: a function will be called when the operation has completed. Syntactically, they look very different:
// Promise
function myDelayFunction(ms) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, ms);
});
}
myDelayFunction(1000).then(function() {
console.log('1000ms have passed!');
})
// Callback
function myDelayFunction(ms, callback) {
setTimeout(function() {
callback();
}, ms);
}
myDelayFunction(1000, function() {
console.log('1000ms have passed!');
})
If you have the option to choose between promises or callbacks, use promises. Especially combined with async/await
, they will make your code much more readable and maintainable.
async/await
I’m sure that you’re familiar with async/await
, or at least have seen it used. How is that related to promises?
Actually, async/await
is syntactic sugar to deal with promises:
-
await
waits for a promise to be resolved or rejected before continuing.
In other words, these two examples do the same:// Using `async/await` async function myAsynchronousFunction() { const value = await myOtherAsynchronousFunction(); console.log('The value was:', value); } // Using "regular" promises function myAsynchronousFunction() { myOtherAsynchronousFunction().then(function(value) { console.log('The value was:', value); }) }
-
a function marked
async
will always implicitly return a promise:async function myAsynchronousFunction() { return 123; // return just a regular numerical value } myAsynchronousFunction().then(function(value) { console.log('The value was:', value); }) // Outputs: "The value was: 123"
This also means that you don’t have to explicitly return a promise:
async function myAsynchronousFunction() { return Promise.resolve(123); // unnecessary use of `Promise.resolve()` }
-
throwing an error inside an
async
function will return a rejected promise:async function myAsynchronousFunction() { throw Error('ohnoes'); } myAsynchronousFunction().catch(function(err) { console.log('Oops, caught an error:', err.message); }); // Outputs: "Oops, caught an error: ohnoes"
Instead of using .then/.catch
, using async/await
is preferred because it makes reading and writing your code much easier. A typical problem with .then/.catch
is the so-called “Promise Pyramid of Doom”:
readDirectory().then(function(files) {
readFile(files[0]).then(function(contents) {
writeFile(files[0], contents + "EXTRA DATA").then(function() {
verifyFile(files[0]).then(function(result) {
console.log('all done!', result);
})
})
})
})
Even though this can be mitigated by “promise chaining”, where instead of calling the .then()
on the promise returned by the “next” step, you return that promise:
readDirectory().then(function(files) {
readFile(files[0]).then(function(contents) {
return writeFile(files[0], contents + "EXTRA DATA");
}).then(function() {
return verifyFile(files[0]);
}).then(function(result) {
console.log('all done!', result);
})
})
But still, using async/await
makes the code much more readable:
const files = await readDirectory();
const contents = await readFile(files[0]);
await writeFile(files[0], contents + "EXTRA DATA");
const result = await verifyFile(files[0]);
console.log('all done!', result);
Caveats
-
await
only works in functions that are markedasync
, and this does not propagate down. In certain situations, for instance when dealing with event handlers, this can be confusing:async function myEventHandler() { eventEmitter.on('some-event', function(data) { const result = await myAsynchronousFunction(data); // FAIL }); }
Even though
myEventHandler
is markedasync
, usingawait
will still yield aSyntaxError
. That’s because the event handler function isn’t markedasync
. To fix:async function myEventHandler() { eventEmitter.on('some-event', async function(data) { // needs to be `async` const result = await myAsynchronousFunction(data); // SUCCESS }); }
In fact, if this is all that
myEventHandler
does, it doesn’t need to beasync
itself:function myEventHandler() { // Doesn't need to be `async` eventEmitter.on('some-event', async function(data) { const result = await myAsynchronousFunction(data); }); }
When to create a new promise
It’s becoming less and less common to have to create promises yourself. Most of Homey’s SDK is based on promises (with callbacks being offered for backward compatibility for existing code), and most external modules also use promises.
One example that still needs manual promise creation is to “promisify” setTimeout
:
function delay(ms) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, ms)
})
}
// Usage:
async function myAsynchronousFunction() {
// delay for 5 seconds before we continue
await delay(5000);
...
}
And another example that I sometimes use in Homey drivers (because, oddly enough, Driver.ready
is a method that only accepts a callback, and doesn’t return a promise):
class MyDriver extends Homey.Driver {
...
waitForReady() {
const driver = this;
return new Promise(function(resolve) {
driver.ready(resolve);
})
}
}
// Usage, for instance inside a device:
class MyDevice extends Homey.Device {
async onInit() {
// Wait for driver to become ready before we continue.
await this.getDriver().waitForReady();
...
}
}
Promise caveats
-
a common anti-pattern is to wrap a promise chain inside a
new Promise
:function myAsynchronousFunction() { return new Promise(function(resolve, reject) { myOtherAsynchronousFunction().then(function(result) { resolve(result); }).catch(function(err) { reject(err); }) }) }
Promise chains always return a promise, which means that the code above can be rewritten to this:
function myAsynchronousFunction() { return myOtherAsynchronousFunction(); }
Yes, that does exactly the same. Only faster and with less code
-
if an asynchronous function offers the option of using promises or callbacks, pick one. Don’t use both:
myAsynchronousFunction(function(result) { // callback }).then(function(result) { // promise handler })
Various tidbits
try/catch
Using try/catch
in JS can be confusing. For instance, this doesn’t work as expected:
try {
myAsynchronousFunction().then(function() {
throw Error('ohnoes');
})
} catch(err) {
...
}
This will actually cause an UnhandledPromiseRejectionWarning
, meaning that the exception wasn’t caught by the catch
. So why is that?
The JS interpreter will run the statements inside of a try
block, and if any of those statements result in an immediate exception, the catch
block gets triggered. But in the example code above, the exception isn’t thrown immediately: it’s thrown when the promise is resolved. But this is done asynchronously, meaning “somewhere in the future”.
It doesn’t matter if myAsynchronousFunction
doesn’t actually do anything besides returning a promise: it is inherent to promises (and any asynchronous function) that their handling will be queued and postponed (specifically, until the call stack is empty). This is too late for the catch
to get involved.
So try/catch
cannot be used for asynchronous code? Well, it can, but only when combined with async/await
. So this will work as expected:
try {
await myAsynchronousFunction();
throw Error('ohnoes')
} catch(err) {
...
}
Rule of thumb: don’t use try/catch
for asynchronous code, except for when you’re using async/await
.
Arrow function notation
I have refrained from using arrow function notation in all of the examples above, for clarity reasons. This doesn’t mean that you shouldn’t use them, and, in fact, I would recommend using them.
Arrow functions are great if your code looks like this:
function someMethod() {
const instance = this:
someFunction().then(function(result) {
instance.setResult(result);
});
}
The problem here is that the value of this
inside the promise success handler is typically useless, so if you want to access the this
from before the call to someFunction
, you have to store it in a temporary variable instance
.
Using arrow functions will solve this (pun intended) by making the value of this
inside the function the same as it was outside the function:
function someMethod() {
someFunction().then(result => {
this.setResult(result);
});
}
Technically speaking, you can do the same with “regular” function expressions too, using Function.prototype.bind
:
function someMethod() {
someFunction().then(function(result) {
instance.setResult(result);
}.bind(this));
}
But it’s much more preferable to use arrow function notation.
As always, there are subtleties involved, and using arrow function notation in some situations may cause problems. Typically, this happens when you’re using external modules that specifically set this
(much like the .bind
example above).
Resolving multiple promises in parallel
Sometimes you need to perform various asynchronous functions at the same time. You can, of course, use this:
const result1 = await myAsynchronousFunction1();
const result2 = await myAsynchronousFunction2();
const result3 = await myAsynchronousFunction3();
This will run each asynchronous function sequentially, one after the other. This isn’t necessarily very efficient, because Node.js is very good at handling concurrent asynchronos functions. So if the functions are independent of each other (that is, the functions don’t depend on the results of the other functions), you can run them concurrently, in parallel, using Promise.all
:
const [ result1, result2, result3 ] = await Promise.all([
await myAsynchronousFunction1(),
await myAsynchronousFunction2(),
await myAsynchronousFunction3()
]);
One caveat is that if any of the functions fails due to an exception, Promise.all
will fail too.