Skip to main content

Asynchronous Functions

Working with the back-end can be confusing; after one asynchronous operation, you need to make another request to the server using the received data, and so on a number of times. For example, on a profile page, a user wants to see a list of friends. The first thing to do is to confirm the user’s access rights to this page from the back-end. To do this, you need to make a request to my-api.com/me. If the back-end allows access, in response you will receive a unique access token for the protected resources.

const fetchFriends = () => {
return fetch("my-api.com/me").then(token => {
console.log(token);
});
};

Next, you need to request a user profile from my-api.com/profile, but the profile is not complete, as it contains only critical information – user ID, without a list of friends.

const fetchFriends = () => {
return fetch("my-api.com/me")
.then(token => {
return fetch(`my-api.com/profile?token=${token}`);
})
.then(user => {
console.log(user.id);
});
};

Only after that, you can request the list of friends from my-api.com/users/:userId/friends.

const fetchFriends = () => {
return fetch("my-api.com/me")
.then(token => {
return fetch(`my-api.com/profile?token=${token}`);
})
.then(user => {
return fetch(`my-api.com/users/${user.id}/friends`);
});
};

fetchFriends()
.then(friends => console.log(friends))
.catch(error => console.error(error));

Not the most readable code, although the operations are relatively simple. Because you pass handler functions to the then() method, you get nesting trees.

Asynchronous functions help get rid of callbacks and nested constructs. At the same time, they work perfectly in conjunction with the then() and catch() methods, as they are guaranteed to return a promise.

const fetchFriends = async () => {
const token = await fetch("my-api.com/me");
const user = await fetch(`my-api.com/profile?token=${token}`);
const friends = await fetch(`my-api.com/users/${user.id}/friends`);
return friends;
};

fetchFriends()
.then(friends => console.log(friends))
.catch(error => console.error(error));

async/await syntax

Asynchronous functions (async/await) is a convenient way to create asynchronous code that looks like synchronous. The async/await syntax is based on promises, so it does not block the main program flow.

To declare an asynchronous arrow function, add the keyword async before the list of parameters. Inside, you can use the await operator and put, to the right of it, something what will return a promise. The response.json() method also returns a promise, so put await.

const fetchUsers = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
return users;
};

fetchUsers().then(users => console.log(users));

When the interpreter sees await, it suspends to execute this function (not the entire script) and waits until the promise to the right of await is executed. As soon as the promise has been executed, the function resumes, and on the line below you will see the result of the asynchronous operation.

  • The await operator can only be used in the body of the async function.
  • The await operator suspends the function until the promise is executed (fulfilled or rejected).
  • If the promise is fulfilled, the await operator will return its value.
  • If the promise is rejected, the await operator will throw an error.
  • An asynchronous function always returns a promise, so any return value will be its value.
  • If you do not specify a return value, a promise with undefined will be returned.

Any function can be asynchronous, be it an object method, class, callback, declaration or inline function. All of them will be able to use the await operator and will always return a promise, since they will be asynchronous functions.

// Function declaration
async function foo() {
// ...
}

// Functional expression
const foo = async function () {
// ...
};

// Arrow function
const foo = async () => {
// ...
};

// Object method
const user = {
async foo() {
// ...
},
};

// Class method
class User {
async foo() {
// ...
}
}

Handling errors

If the result of an asynchronous function (promise) is not used in outer code, errors are handled in the function body by the try...catch construct. The error parameter value in the catch block is the error that await will generate if the promise is rejected.

const fetchUsers = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
console.log(users);
} catch (error) {
console.log(error.message);
}
};

fetchUsers();

If the result of an asynchronous function (promise) is used in outer (global) code, that is, outside of other asynchronous functions, errors are handled by the catch() method’s callback. The error parameter value in the catch() method is the error that await will generate if the promise is rejected.

const fetchUsers = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
return users;
};

fetchUsers()
.then(users => console.log(users))
.catch(error => console.log(error));

This will not work: await can only be used in the body of an asynchronous function.

const fetchUsers = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
return users;
};

// ❌ SyntaxError: await is only valid in async function
const users = await fetchUsers();

If the result of an asynchronous function is used in another asynchronous function, errors are handled by the try...catch construct. The error parameter value in the catch block is the error that await will generate if the promise is rejected.

const fetchUsers = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
return users;
};

const doStuff = async () => {
try {
const users = await fetchUsers();
console.log(users);
} catch (error) {
console.log(error.message);
}
};

doStuff();

Parallel requests

If you need to make multiple requests at the same time, use the async/await syntax very carefully. In the following example, you will see three sequential requests, as the execution of the asynchronous function is suspended when the interpreter sees await. In addition, request results will also be parsed sequentially, which will take more time.

const fetchUsers = async () => {
const baseUrl = "https://jsonplaceholder.typicode.com";
const firstResponse = await fetch(`${baseUrl}/users/1`);
const secondResponse = await fetch(`${baseUrl}/users/2`);
const thirdResponse = await fetch(`${baseUrl}/users/3`);

const firstUser = await firstResponse.json();
const secondUser = await secondResponse.json();
const thirdUser = await thirdResponse.json();

console.log(firstUser, secondUser, thirdUser);
};

fetchUsers();

In the Network tab, you can clearly see that each subsequent request waits for the previous one to be completed. That is, they are executed sequentially, which takes more time (total duration of all requests). This is fine when requests depend on each other, i.e. the next one uses the result of the previous one.

Concurrent requests

In our case, they are completely independent, so you need to run them in parallel. For this, create an array of promises, after which use the Promise.all() to wait for their execution. An array of promises is created by such methods as map(), filter(), etc., depending on the task.

const fetchUsers = async () => {
const baseUrl = "https://jsonplaceholder.typicode.com";
const userIds = [1, 2, 3];

// 1. Create an array of promises
const arrayOfPromises = userIds.map(async userId => {
const response = await fetch(`${baseUrl}/users/${userId}`);
return response.json();
});

// 2. Run all promises in parallel and wait for their completion
const users = await Promise.all(arrayOfPromises);
console.log(users);
};

fetchUsers();

With this approach, requests are run in parallel, which saves waiting time for their execution (it is equal to the duration of the "slowest" of them). This technique is only suitable if the requests are independent of each other.

Parallel requests

Check this by opening the developer tools in a live example. Also, a button was added that is used for making requests; a potential error was handled with the try...catch construct. This is standard AJAX code using asynchronous functions.