Overview
Asynchronous calls, such as HTTP get requests via fetch
, axios
, AJAX
or other asynchronous calls via Promises
all behave similarly in that what is returned by the time that the function reaches its last line is not actually the result that you want.
Explanation of Issue
This is because asynchronous calls do not wait for the call to finish executing before executing the next line:
Note that in all the examples below, I will be using fetch
but the concept could easily be extended to the other call types.
getDataWithFetch();
function getDataWithFetch() {
console.log('start of outer function');
const url = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(url).then(response => {
console.log('start of async function');
response.json();
console.log('end of async function');
});
console.log('end of outer function');
}
As you can see via Run code snippet, the outer function
starts and ends before the async function
starts. What is undefined
being returned from getDataWithFetch()
because it is hitting the end of the function, and has no return
statement. If we change the code to add a return statement, we can see what part of the function call is actually being returned:
const response = getDataWithFetch();
console.log(response);
function getDataWithFetch() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(url).then(response => {
return response.json();
});
return 'hello world';
}
Now the function isn't returning undefined
anymore, but it's not returning the fetch
request response; instead, it is returning 'hello world'
. This is because the fetch
request is not blocking the rest of the getDataWithFetch
function from executing. To understand why, we need to understand what fetch
is doing behind the scenes.
Promises
Resolve
To do so, we need to understand Javascript Promises. There's a great beginner-friendly read on it here: https://javascript.info/promise-basics, but in a nutshell:
A Promise is an object that, when it finishes the asynchronous work, it will call the "resolve" function. Where "resolve" returns the expected data via resolve(dataToReturn)
.
So behind the scenes, fetch
returns a Promise
that will call resolve
when the fetched data is retrieved, like so:
// just an example of what fetch somewhat looks like behind the scenes
function fetch(url) {
return new Promise(resolve => {
// waiting for response to come back from server
// maybe doing a bit of additional work
resolve(responseDataFromServer);
});
}
With that understanding, we now know that we need to return the call to fetch
to actually return the Promise
, like so:
const response = getDataWithFetch();
console.log(response);
function getDataWithFetch() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
return fetch(url).then(response => response.json());
}
The console.log
is no longer returning undefined
, but it's not returning the right response either. That's because getDataWithFetch
is now returning the Promise
object from fetch
. It's time to get the response from resolve
.
.then
To get the data passed to the resolve
function, we need to use the then
method on the returned Promise
.
A Promise
provides the method then
to retrieve the value passed to resolve
as such: myReturnedPromise.then(dataToReturn => { /* do something with the data */ });
. then
will only execute once the dataToReturn
is ready to be handled.
Final Solution
So by using the then
method, we can get the actual response printed to console:
getDataWithFetch().then(response => {
console.log(response);
});
function getDataWithFetch() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
return fetch(url).then(response => response.json());
}
And there you have it!
Overly Verbose Solution
If there was too much magic in what made that solution work, you can also see the breakdown of it by explicitly returning a Promise
to be fulfilled:
getDataWithFetch().then(response => {
console.log(response);
});
function getDataWithFetch() {
return new Promise(resolve => {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
return fetch(url).then(response => {
resolve(response.json()));
// Note that the `resolve` does not have to be part of the `return`
}
});
}
Async and Await Solution
Recent improvements to Javascript have provided us with a more succinct way of handling Promises
to make them appear synchronous instead of asynchronous. Note that the following code is equivalent to the above solutions, but much easier to read:
async function getDataWithFetch() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = await fetch(url);
return response.json();
}
getDataWithFetch().then(response => {
console.log(response);
});
async
and await
are clean and powerful alternatives to explicitly dealing with Promises
, especially when Promise Chaining is involved. I recommend reading more about them here: https://javascript.info/async-await.