Promises Explained With Examples
In this video you will learn what are callbacks, promises and async await in javascript, how to write them and what problems do they solve.
So let's jump right into it.
To understand the necessity of promises we need first understand what problem they are solving. In Javascript we sometimes need to make a long running operation. The most popular example of long operation is making API request. This is called asynchronous operation. The problem that we have in javascript compared to other languages that we can't make asynchronous operations inline one by one and wait until they are finished.
And there are several solutions to this. It's collbacks, promises and async await. The oldest thing that exist is callbacks. Let's check on the example how we can write it and the write the same code to promises.
const nirvanaUrl =
"http://musicbrainz.org/ws/2/artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da?fmt=json";
const metallicaUrl =
"http://musicbrainz.org/ws/2/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab?fmt=json";
const getJSON = (url, callback) => {
fetch(url)
.then((response) => response.json())
.then((data) => {
callback(data);
});
};
getJSON(nirvanaUrl, (nirvanaData) => {
console.log("get nirvanaData", nirvanaData);
});
So here I have a getJSON function. We will talk how exactly it works later. For now it's interesting that it is written exactly in callback way. We pass an url that we want to fetch and the callback. Callback is our own function which will be executed when our asynchronous getJSON function finishes.
Also here we are using musicbrainz API. It's a public music API so we can use it even without registration.
So what is the problem with callback? Problem starts when we need more than one asynchronous function. Let's say that we need to fetch metallica additionally and only when we have data of both bands we need to do something.
getJSON(nirvanaUrl, (nirvanaData) => {
getJSON(metallicaUrl, (metallicaData) => {
console.log("get both", nirvanaData, metallicaData);
});
});
As you can see it works but we start getting a "javascript callback hell". It's a term when we nest too many asynchronous operations together. Here we have only 2 but just imagine that we have 10 of them. This will be completely unsupportable.
And this is exactly the point why promise were created in javascript. To solve problems that we have with callbacks. So what is a Promise? It's an thing that can do asynchronous operation inside. When it is done successfully we are saying that the promise is resolved and when it fails then it was rejected.
Let's try to create a simple promise.
const promise = new Promise((resolve, reject) => {
// some async code
if (1 !== 2) {
resolve();
} else {
reject();
}
});
So we create an instance of Promise and pass a function inside. This function has 2 arguments. Resolve and reject functions. Inside we can do whatever code we need, any asynchronous code will also work because at the end of it we will just call resolve or reject as we did with callback.
Let's check now how we can use thing promise to subscribe to it's data.
promise
.then(() => {
console.log("success");
})
.catch(() => {
console.log("error");
});
So now on our promise we can call then and catch. Then means that it was success and catch that we had an error. Inside then and catch we are also passing our callback function what we want to execute.
As you can see in browser we are getting back success.
Now let's look on our getJSON function.
const getJSON = (url, callback) => {
fetch(url)
.then((response) => response.json())
.then((data) => {
callback(data);
});
};
So you can see the same syntax as we used when we created a promise. We have then. Which actually means that when we use fetch to get some data from API it returns us promise.
Now the question is why promises are better than callbacks? Because the code looks a bit similar. First of all we have a new entity "Promise" which abstracts asynchronous code for us. In case with callbacks we can write them how we want but with all promises we are working in the same way with then and catch.
Secondly we can avoid callback hell. Let's rewrite our nested code with promises.
fetch(nirvanaUrl)
.then((response) => response.json())
.then((nirvanaData) => {
console.log("nirvanaData", nirvanaData);
});
So here we used plain fetch because we know that it returns a promise. As you can see our single call works. But now we can make a second call because inside then we can return a promise and stack them.
fetch(nirvanaUrl)
.then((response) => response.json())
.then((nirvanaData) => {
console.log("nirvanaData", nirvanaData);
return fetch(metallicaUrl)
.then((response) => response.json())
.then((metallicaData) => {
return { metallica: metallicaData, nirvana: nirvanaData };
});
})
.then((res) => {
console.log("both", res);
});
In may be a bit complex to understand but we can stack several thens and pass a value from previous to next. What is more important is that we can return there a promise and we will get in next then only when first then is completed. So even if we need to add 10 additional requests here we can make it flat are just combine previous data with next. This is exactly the avoidance of callback hell.
You might say that this code is really complex for such easy task as just getting single result from several requests. And you are totally right. There is a better way. in Promise call we have all method which wraps an array of promises in single promise. Let's rewrite this code using Promise.all
const nirvanaPromise = fetch(nirvanaUrl).then((response) => response.json());
const metallicaPromise = fetch(metallicaUrl).then((response) =>
response.json()
);
Promise.all([nirvanaPromise, metallicaPromise]).then((result) => {
console.log("result all", result);
});
Here we first created 2 promises and then used Promise.all which will be resolved only if all promises return success. It's really nice way of doing things if all promises are independent and you can run them in parallel. Unfortunately if you need data from first request in a second then you still need to use our previous approach.
Now you might ask. Okay we now know what is Promise but actually we used everywhere fetch which gives us Promise. Do we need to create it by ourselves?
Actually not that often but yes if you want to make something low level or for example you have some old library on callbacks which you want to use.
Let's create a promise wrapper for our getJSON function which has callback approach.
const getJSON = (url, callback) => {
fetch(url)
.then((response) => response.json())
.then((data) => {
callback(data);
});
};
const getJSONAsPromise = (url) => {
return new Promise((resolve) => {
getJSON(url, (data) => {
resolve(data);
});
});
};
getJSONAsPromise(nirvanaUrl).then((res) => {
console.log("result", res);
});
We created getJSONAsPromise function which wraps our callback approach in promise. We simply call resolve when we need to.
Let's look once again on our complex example where we returned next promise inside then. This code has 1 big problem which may be not clear on the first glance. We make different things and every time we use .then no make our code more readable. So every then has it's own isolated function. This essentially means that we must pass all properties down in the next then if we need it later. And of course it's not convenient.
Also as I said previously in other languages we don't have such async problems. We simply write there async code inline like synchronous and it just works out of the box.
We kinda get something similar with the newest approach to work with asynchronous operations and it's an async await.
In order to use it we need to create an asynchronous function. Which essentially means that we can't write async await in the root level.
const fetchBands = async () => {
}
As you can see we simply write async word before round brackets. Inside async functions we can use await operator which means that we want to wait for some promise to be resolved.
const fetchBands = async () => {
const nirvanaResponse = await fetch(nirvanaUrl);
const nirvanaData = await nirvanaResponse.json();
console.log("nirvanaData", nirvanaData);
};
fetchBands();
Because we need a plain data we must call json on the response with await once again. So every time when we need to resolve a promise we write await. The main benefit is that all our code in completely inline, easy to read and understand. Also we still work with promises everywhere so async await is just a sugar for them.
So now fetch both call and then do something with results is super easy because it's the single function and all variables are local.
const fetchBands = async () => {
const nirvanaResponse = await fetch(nirvanaUrl);
const nirvanaData = await nirvanaResponse.json();
const metallicaResponse = await fetch(metallicaUrl);
const metallicaData = await metallicaResponse.json();
console.log("both", nirvanaData, metallicaData);
};
fetchBands();
The only this that you need to know about async await is how to catch errors if one of request fails. If we simply make the domain not correct we are getting
main.js:31 Uncaught (in promise) TypeError: Failed to fetch
This is because we didn't handle an error in fetch. The common practice for this is to wrap all async await code in try catch block.
try {
const nirvanaResponse = await fetch(nirvanaUrl);
const nirvanaData = await nirvanaResponse.json();
const metallicaResponse = await fetch(metallicaUrl);
const metallicaData = await metallicaResponse.json();
console.log("both", nirvanaData, metallicaData);
} catch (err) {
console.log("err", err);
}
As you can see now we don't have an uncaught error and we can handle our errors in catch block.
This was everything that you need to know about asynchronous operations in javascript. Today nobody use callbacks. People are using promises with then or async await.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!