Introduction
Asynchronous programming is a must-have in modern JavaScript development, allowing developers to perform non-blocking operations, such as fetching data from a server, reading files, or executing time-consuming operations.
ES2017 introduced async functions and the await
keyword that are a complete game changer in asynchronous development.
This blog post is a guide to using async/await to handle asynchronous tasks in an elegant way.
How To Use Async/Await
async/await
statements allow developers to write asynchronous code that looks and behaves a like synchronous code:
jsasync function fetchDataAsync() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } const data = await fetchDataAsync(); console.log(data);
Here an async
function returns a Promise<T>
, that holds a data received from the API call.
By using await
keyword we get this data as a promise result.
After the line const data = await fetchDataAsync();
we can simply write more code as if all operations were executed synchronously.
async/await
offer an elegant way for executing asynchronous operations represented by JavaScript Promises.
To learn more about promises read my blog post.
await
keyword is only allowed to be used in the async
functions.
jsfunction test() { const data = await fetchDataAsync(); // Syntax error } async function test() { const data = await fetchDataAsync(); // Now ok }
Async/Await in Top-Level Statements
With the introduction of ECMAScript 2022, JavaScript supports top-level await
statements in modules.
This allows you to use await
outside of async functions within modules, simplifying the initialization of resources.
jsconst response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); console.log(data);
Outside modules or in older versions of web browsers, you can use the following trick with anonymous async function to use await
in top-level statements:
js(async () => { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); console.log(data); })();
Async Methods as Class Members
You can encapsulate asynchronous logic within objects by defining async methods in JS classes.
It is a good practise to add Async
suffix when naming asynchronous functions.
jsclass PostService { async getPostsAsync() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } } const postService = new PostService(); const data = await postService.getPostsAsync(); console.log(data);
Error Handing When Using Async/Await
Error handling when using async/await
is straightforward by using try/catch
statement:
jsasync function fetchDataAsync() { try { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch data: ", error); } }
When a reject
method is called while awaiting a promise - an exception is thrown, that can be handled in the catch
block:
jsfunction testPromise() { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error("Test Error")); }, 1000); }); } try { const response = await testPromise(); } catch (error) { console.error("Failed to get result: ", error); }
Using Promise Utility Methods With Async/Await
Promise class has few utility static methods for asynchronous programming:
- Promise.all
- Promise.any
- Promise.race
- Promise.allSettled
Promise.all
You can use Promise.all
function to wait for multiple promises to resolve.
This function takes an array of promises and returns a new promise that resolves when all of the promises have resolved, or rejects if any promise is rejected.
This method is particularly useful when you have multiple asynchronous operations that can be executed in parallel.
Let's explore an example, where we fetch posts, comments and todos using Promise.all
:
jsasync function fetchMultipleResourcesAsync() { const urls = [ "https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments", "https://jsonplaceholder.typicode.com/todos" ]; try { const promises = urls.map(url => fetch(url)); const responses = await Promise.all(promises); const data = await Promise.all(responses.map(res => res.json())); return data; } catch (error) { console.error("Error fetching one or more resources:", error); } return null; } const data = await fetchMultipleResourcesAsync(); console.log("Posts, comments, todos:", data);
It will be more efficient to fetch this data in parallel than fetching posts, comments and todos one by one.
Promise.any
You can use Promise.any
function to wait for one of multiple promises to resolve.
This function takes an array of promises and returns a single promise that resolves when the first of the promises is resolved.
If all the promises are rejected, then the returned promise is rejected with an AggregateError, an exception type that groups together individual errors.
jsasync function fetchFirstResourceAsync() { const urls = [ "https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments", "https://jsonplaceholder.typicode.com/todos" ]; try { const promises = urls.map(url => fetch(url)); const firstResponse = await Promise.any(promises); const data = await firstResponse.json(); return data; } catch (error) { console.error("All requests failed:", error); } return null; } const data = await fetchFirstResourceAsync(); console.log("First available data:", data);
Promise.race
Promise.race
is similar to Promise.any
, but it completes as soon as one of the promises is either resolved or rejected.
This method is useful for timeout patterns when you need to cancel request after a certain time.
jsasync function fetchDataWithTimeoutAsync() { const fetchPromise = fetch("https://jsonplaceholder.typicode.com/comments"); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), 5000)); try { const response = await Promise.race([fetchPromise, timeoutPromise]); const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch or timeout reached:", error); } return null; } const data = await fetchDataWithTimeoutAsync(); console.log("Comments received:", data);
Promise.allSettled
You can use Promise.allSettled
function to wait for all the promises to complete, regardless of whether they resolve or reject.
It returns a promise that resolves after all the given promises have either resolved or rejected.
This promise contains an array of objects where each describes the result of each promise.
jsasync function fetchMultipleResourcesAsync() { const urls = [ "https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments", "https://jsonplaceholder.typicode.com/todos" ]; try { const promises = urls.map(url => fetch(url)); const results = await Promise.allSettled(promises); const data = results.map((result, index) => { if (result.status === "fulfilled") { console.log(`Promise ${index} fulfilled with data:`); return result.value; // Collecting fulfilled results } else { console.error(`Promise ${index} rejected with reason:`, result.reason); return null; // You might want to return null or a default object } }); return data; } catch (error) { console.error("Error fetching one or more resources:", error); } return null; } const data = await fetchMultipleResourcesAsync(); console.log("Posts, comments, todos:", data);
Awaiting Thenable Objects
In JavaScript, a thenable is an object or function that defines a then
method.
This method behaves similarly to the then
method found in native promises.
That way async/await
can handle these objects just like regular promises.
For example:
jsclass Thenable { then(resolve, reject) { setTimeout(() => resolve("Task completed"), 1000); } } const result = await new Thenable(); console.log(result);
Thenables objects can be useful for integrating with systems that don't use native promises but have promise-like behavior. However, thenable objects can introduce confusion to the source code as their behavior is not straightforward.
Native promises are preferable due to their comprehensive feature set and better integration with the JavaScript ecosystem.
Hope you find this blog post useful. Happy coding!