Notes on Promises and Useful Snippets (ft. async and await)
Ed
Posted on January 3, 2021
Quite a few people in my circle are either in development or looking to get into it as a career. The majority of them are learning JavaScript and the questions that I get asked most often relate to promises in some way.
I thought it would be a good idea to write up a brief overview of promises, explaining what they are (on a high level) and go through some snippets that I find myself using in my day to day.
While I did try to make this post as beginner friendly as possible, I assume that you, the reader, will have at least a basic understanding of JavaScript. If you don't, I highly recommend the CodeCademy Introduction to JavaScript.
What Exactly is a Promise?
In simple terms, it's just a way for us to execute a bit of code and provide the result of that code at some point in the future.
Imagine having a function that can execute in the background, while the rest of your application keeps happily chugging along, reacting to any button clicks, updating the DOM and etc. Once that function finishes executing (the Promise resolves), we resume the execution path that requires the result of that function.
The most common use case for promises is making API calls. You'd instruct your application to send a request to an API and do something with the result once your application receives a response. While that's happening in the background, or asynchronously, you can still keep using the application.
However, it's not just API calls that promises are useful for. In a broader sense, we use promises whenever we don't want to sit around waiting for IO - reading from or writing to disk, network requests or even intensive CPU tasks are some of the other use cases for promises.
It might be a bit difficult to imagine still, but bare with. The examples should help conceptualize the idea of a promise a bit better.
Here are the two main ways to use Promises - the standard API and the more novel async
and await
:
// We return a promise object that can either resolve (success) or reject (failure)
function promised() {
return new Promise(function(resolve, reject) {
return resolve('yay!');
});
}
// We call our promised() function and then follow it up with a .then()
// The function inside .then() will execute
// immediately after the promise resolves.
// The result of your promise will be passed in
// as a parameter of our callback function.
promised().then(function(promiseResult) {
console.log(promiseResult);
});
// Should print out 'yay!'
// Because we want to use await at the top level
// we have to wrap our code in a self-executing async function.
// This "hack" has a story of its own, I'll include it
// in Further Reading, but will not go over it here in much detail.
(async () => {
// async here just says that whatever this function returns
// should be wrapped in a promise.
// adding the sync keyword to our function also allows us to
// use await within the context of that function.
async function promised() {
return 'yay!';
}
console.log(await promised());
// Should print out 'yay!'
})();
Disregarding the self-executing async
wrapper, the code using async
and await
looks much neater and, in most cases, is going to be preferred. However, we still need to know and understand the previous method since there are times when it's useful.
Useful Snippets
In this section I'll cover some snippets that I use in my day to day that I think might be useful to others as well. They range from quite basic to more advanced. I highly recommend playing around with each snippet, to get more of an understanding of each of their intricacies.
Promise Chaining
This is a bit of a basic one, but possibly the most important. One of the great things about promises is that they can be chained together. Meaning, we can force sequential execution.
Lets say we want to fetch a fake person from one API and then use another API to guess our fake persons age by their name - a completely logical thing to do. Here's what it'd look like:
function fetchFakeUser() {
// fetch() will return a promise.
return fetch('https://randomuser.me/api/');
}
function fetchAge(name) {
return fetch('https://api.agify.io/?name='+name);
}
fetchFakeUser()
.then((fakeUserResponse) => {
// Get the JSON data from the response. Returns a Promise.
return fakeUserResponse.json();
})
// As soon as the Promise returned by json() resolves
// we'll continue executing the .then() chain.
// Note that the result returned by the previous .then()
// will be passed in as a parameter to our next .then() call
.then((fakeUserData) => {
// Return the name of our fake user down the Promise chain.
return fakeUserData.results[0].name.first;
})
.then((name) => {
console.log('Name: '+name);
return fetchAge(name);
})
// We'll wait for the Promise returned by fetchAge to resolve,
// then continue executing the chain.
.then((fetchAgeResponse) => {
return fetchAgeResponse.json();
})
.then((data) => {
console.log('Age: '+data.age);
});
We can keep chaining the .then()
functions indefinitely, as long as we want to maintain that sequential control.
One particular benefit of this is that it keeps our code relatively clean. Try and imagine doing something like this with nested callbacks, that'd be absolute hell!
We can also convert the above to use the async
and await
notation. If we did, it would look like this:
(async () => {
// The functions below don't need to be prefixed
// with async, because fetch() already returns a Promise,
// so we don't need to do any "wrapping" ourselves.
function fetchFakeUser() {
// fetch() will return a promise.
return fetch('https://randomuser.me/api/');
}
function fetchAge(name) {
return fetch('https://api.agify.io/?name='+name);
}
// We'll use await to wait until the Promise
// returned by our function resolves.
const fakeUserResponse = await fetchFakeUser();
// Will only resume execution after the above Promise resolves.
const fakeUserData = await fakeUserResponse.json();
const name = fakeUserData.results[0].name.first;
console.log('Name: '+name);
const fetchAgeResponse = await fetchAge(name);
const fetchAgeData = await fetchAgeResponse.json();
console.log('Age: '+data.age);
})();
The above is more or less a direct translation of our implementation using .then()
chains. One thing to note though is that everything below an await
will be executed only after that function completes. So if we're awaiting for an API request, anything that comes after will be executed only after the request completes. This is particularly important to remember if you're using await
and want to execute multiple promises at the same time (or in parallel). We'll get to this in another snippet.
Error Handling
One thing we've not touched on just yet has been error handling. As with anything, we want to be able to catch any errors that our promises throw and gracefully handle them. With promises, there are a few different ways we can approach this.
Using .then() and .catch()
It's fairly straightforward when we're using .then()
- we'll use .catch()
.
const alwaysError = new Promise((resolve, reject) => {
throw new Error('Oops!');
resolve('Success!');
});
alwaysError
// The function passed into .catch()
// will receive the error as its parameter.
// We can also return something from the .catch()
// and continue our promise chain further.
.catch((error) => {
// console.log(error.message);
return 'Failed!';
})
.then((userMessage) => {
// If we would not have thrown an error,
// our message would be 'Success'
// as the catch() function is never triggered.
// You can try this by commenting out
// the "throw new Error" above.
console.log(userMessage);
});
If an error is thrown anywhere up the promise chain, .catch()
will intercept it and it will immediately skip to executing the function that was passed into it. Once .catch()
finishes executing, the rest of the promise chain can continue with the value returned in the event of failure. Easy peasy, right?
Using try and catch
Using async
and await
we'll want to use try
and catch
for our error handling. The only thing I'd like to draw your attention to here is that we have also extracted the error handling to a separate function:
(async () => {
const alwaysError = async () => {
// Comment the error out
// to see the success flow.
throw new Error('Oops!');
return 'Success!';
};
const getMessage = async () => {
try {
return await alwaysError();
} catch (error) {
// Any error that is thrown by our promise
// or if we manually call the reject method
// will trigger this catch block.
return 'Failure!';
}
};
const message = await getMessage();
console.log(message);
// Should print out "Failure!"
})();
By doing the above, we nicely encapsulate our logic of "getting a message" along with any error handling.
Using await and .catch()
Sometimes extracting your error handling into a separate function might feel like overkill. Maybe you just want to quickly catch, recover and continue execution without any extra overhead. Using the try/catch
approach we run into a few issues:
(async () => {
const alwaysError = async () => {
// Feel free to comment this error out
// to see how it'd work without.
throw new Error('Oops!');
return 'Success!';
};
try {
const message = await alwaysError();
console.log(message);
} catch (error) {
// Handle our error here.
const message = error.message;
console.log(message);
}
// But if we want to use anything
// outside our try/catch block,
// it will not be available.
console.log(message);
// Message in this context will be "undefined"
// and you will likely get an error.
})();
The main problem with this example is that nothing is available outside our try/catch
block. There are ways to solve this, but none of them are elegant:
- Declare
message
usinglet message
just before ourtry/catch
block, making it available outside the block scope. This, however, leaves us with a dangling, reassignable variable, so is not ideal. - Just stick all our code in the
try/catch
blocks. But this will increase nesting and very likely also lead to code duplication.
A cool and quick way to handle the above problem that I have found is to use a mix of await
and .catch()
:
(async () => {
const alwaysError = async () => {
// Comment the error out
// to see the success flow.
throw new Error('Oops!');
return 'Success!';
};
const message = await alwaysError().catch((error) => { return 'Failure!'; });
console.log(message);
// Should print out "Failure!"
})();
The above works because .catch()
and alwaysError
both return a Promise and in this scenario await
will wait for whichever Promise was returned last to resolve. This gives us a very elegant way to recover from an error that was thrown by our function and continue execution as if nothing happened.
Personally, I really like this approach and would even prefer it to try/catch
in most cases, due to how clean and simple it is.
Parallel Execution
When talking about promise chaining using await
, we briefly touched on parallel execution. Going back to our example of getting a fake person from an API, lets pimp it out a bit. Lets try and guess the age, country and gender of the name that we get.
A common solution to a problem like that would be something along the lines of:
(async () => {
// We're prefixing the function with async
// because we're going to be using await inside it.
async function fetchFakeName() {
const response = await fetch('https://randomuser.me/api/');
const data = await response.json();
return data.results[0].name.first;
}
async function fetchAge(name) {
const response = await fetch('https://api.agify.io/?name=' + name);
const data = await response.json();
return data.age;
}
async function fetchCountry(name) {
const response = await fetch('https://api.nationalize.io/?name=' + name);
const data = await response.json();
return data.country[0].country_id;
}
async function fetchGender(name) {
const response = await fetch('https://api.genderize.io/?name=' + name);
const data = await response.json();
return data.gender;
}
const name = await fetchFakeName();
const age = await fetchAge(name);
const country = await fetchCountry(name);
const gender = await fetchGender(name);
console.log(name, age, country, gender);
})();
In this example, we'd wait until each API call was done. This happens because each await
will stop executing anything below it until the promise resolves. A good way around this is to use the Promise.all()
function:
(async () => {
// We're prefixing the function with async
// because we're going to be using await inside it.
async function fetchFakeName() {
const response = await fetch('https://randomuser.me/api/');
const data = await response.json();
return data.results[0].name.first;
}
async function fetchAge(name) {
const response = await fetch('https://api.agify.io/?name=' + name);
const data = await response.json();
return data.age;
}
async function fetchCountry(name) {
const response = await fetch('https://api.nationalize.io/?name=' + name);
const data = await response.json();
return data.country[0].country_id;
}
async function fetchGender(name) {
const response = await fetch('https://api.genderize.io/?name=' + name);
const data = await response.json();
return data.gender;
}
// We fetch a fake name first.
const name = await fetchFakeName();
// Promise.all() will execute all the promises
// that we pass to it at the same time
// and it will return a Promise,
// resolving with all the values of our functions.
const [age, country, gender] = await Promise.all([
fetchAge(name),
fetchCountry(name),
fetchGender(name)
]);
console.log(name, age, country, gender);
})();
Promise.all()
will take our functions, all of which return promises, and it will await until all of them have resolved. One thing to note that's rather important is that if one of the promises throws or rejects, Promise.all()
will immediately reject as well.
Not really parallel, but as parallel as you can get on a single thread.
Racing
Promise.race()
is a bit of a weird one. It's very similar to Promise.all()
where it takes an array of promises in and it returns a single promise back. But unlike Promise.all()
it will not wait until all the promises you give it will resolve. Instead, Promise.race()
will resolve or reject as soon as soon as the first promise given rejects or resolves.
The two primary use cases for it that I've found are for loading indicators and performance checks.
In terms of a performance check, you can fire off requests to multiple endpoints, and you'll resolve with the response from the one that completes first. Fairly straightforward.
Loading indicators is where it gets slightly more interesting. Lets say you're making an API call that you know can take up anywhere from 10ms to 5s and in case of it taking too long, you want to provide the visitor some visual feedback so that they don't navigate away. Here's a basic example of what that would look like:
(async () => {
async function fetchFakeName() {
const response = await fetch('https://randomuser.me/api/');
const data = await response.json();
// Wait 5 seconds before returning the response of our API call.
// This will help us simulate a slow network.
return new Promise((resolve) => {
setTimeout(() => resolve(data.results[0].name.first), 5000);
});
}
function showLoading() {
// Wait 0.5 seconds before letting the user know
// the request is taking longer than usual.
return new Promise((resolve, reject) => {
setTimeout(() => reject('This is taking a while. Please wait!'), 500);
});
}
await Promise.race([
fetchFakeName().then((name) => console.log(`Name: ${name}`)),
showLoading()
]).catch((message) => console.log(message));
// Should print out
// This is taking a while. Please wait!
// Name: [name]
})();
One thing to keep in mind is that the other promises will not cancel and will still complete in the background.
Sequential Execution
While promises are great for executing various tasks asynchronously, sometimes we want to make sure that we are executing certain actions in a sequence. Due to the nature of promises, this can prove quite difficult, but combining promises with Array.reduce()
we can solve this issue:
(async () => {
// The number of processors
// that we have in our pipeline
// can be completely dynamic,
// as long as they accept a string and return a string.
const processors = [
async (name) => name.toUpperCase(), // Convert to uppercase
async (name) => 'Name: ' + name // Prefix with Name
];
// We are utilising Array.reduce here
// and reduce our array of promises to a single promise.
const processName = (initialName) => processors.reduce(
// Our reduce callback is going to take the result
// of the previous (or initial) promise,
// wait for it to be processed and
// pass its result into the next promise.
// processName will return the very last promise from the array.
async (processed, processor) => processor(await processed),
Promise.resolve(initialName)
);
const processedName = await processName('Ed');
// Should print out Name: ED
console.log(processedName);
})();
I have personally found this extremely useful when trying to build data processing pipelines in JavaScript. Or in other words - in cases where you have a piece of data (a JSON object, for example) and you want to pass that JSON object through a series of asynchronous processors.
Closing Notes
I hope people find this compilation useful. I highly recommend you read some of the material linked in Further Reading & References, especially if you are new and find promises hard to grasp still.
If you have any questions or would like to discuss or provide feedback - feel free to shout at me on Twitter @SkepticalHippoh.
Further Reading & References:
- Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
- Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- Promise.all(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
- Promise.race(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
- Array.reduce(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
- Return Early: https://dev.to/jpswade/return-early-12o5
- Callback Hell: http://callbackhell.com/
- How can I use async await at the top level: https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level
- "What the heck is the event loop anyway?" by Philip Roberts: https://www.youtube.com/watch?v=8aGhZQkoFbQ
Posted on January 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024