Rohan Logo
Rohan's Blog
Jan 25, 20245 min read

Asynchronous JavaScript: Callbacks, Promises, and Async/Await

Asynchronous JavaScript: Callbacks, Promises, and Async/Await

Originally designed to make web pages alive, JavaScript has come a long way from its humble beginnings. Today, it’s a powerful language used for both client-side and server-side development, dealing with operations that may not be complete instantly — like fetching data from a server.
This is where asynchronous JavaScript comes into play, allowing the language to perform tasks without blocking the execution thread, improving web application performance and user experience. This article delves into the core of asynchronous JavaScript, exploring Callbacks, Promises, and the Async/Await syntax, each with its own set of examples.

Callbacks: The Foundation of Asynchrony in JavaScript

Callbacks are functions passed into another function as arguments, which the outer function can invoke to perform some action. In the context of asynchronous operations, callbacks ensure that certain code doesn’t execute until other code has finished execution.

Example:

1function download(url, callback) {
2    setTimeout(() => {
3        // simulate a file download
4        console.log(`Downloading ${url} ...`);
5        callback(url); // call the callback function
6    }, 3000);
7}
8
9function process(file) {
10    console.log(`Processing ${file}`);
11}
12
13download('http://example.com/file', process);

Issues with Callbacks:

  • Callback Hell: Also known as the “Pyramid of Doom,” it refers to the heavy nesting of callback functions, which makes the code hard to read and maintain.
  • Inversion of Control: The control over the execution is inverted to the caller of the callback, potentially leading to issues like the caller not calling the callback or calling it too many times.

Best Practice: To mitigate the issues of Callback Hell, use named functions instead of anonymous functions and modularize the code. However, for more complex cases, consider using Promises.

Promises: Escaping Callback Hell

Promises are objects representing the eventual completion or failure of an asynchronous operation. They are a cleaner, more robust solution for asynchronous operations compared to callbacks.

States of a Promise

A promise can be in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation was completed successfully.
  • Rejected: The operation failed.
1let promise = new Promise(function(resolve, reject) {
2    setTimeout(() => resolve("done"), 1000);
3});
4
5promise.then(
6    result => console.log(result), // shows "done" after 1 second
7    error => console.log(error) // doesn't run
8);

Promise Methods:

  1. Promise.all: Waits for all promises to be resolved, or for any to be rejected. If any promise is rejected, it is rejected with the reason of the first promise that was rejected.
1Promise.all([
2    Promise.resolve(1),
3    Promise.resolve(2),
4    Promise.resolve(3),
5]).then(values => console.log(values)); // [1, 2, 3].

2. Promise.allSettled: Waits for all promises to be settled, regardless of the result. The resulting array has { status, value } for fulfilled promises and { status, reason } for rejected ones.

1Promise.allSettled([
2    Promise.resolve(1),
3    Promise.reject('error'),
4    Promise.resolve(3),
5]).then(results => console.log(results));

3. Promise.race: Waits for the first promise to be settled. Whether the promise is fulfilled or rejected, the result of the first promise is the result of Promise.race.

1Promise.race([
2    new Promise((resolve, reject) => setTimeout(() => resolve(1), 500)),
3    new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 100)),
4]).then(console.log); // Error: Whoops!

4. Promise.resolve / Promise.reject: Returns a new Promise object that is resolved with the given value, or rejected with the given reason.

Best Practice: Use Promise.all for cases where you need all promises to resolve, and Promise.allSettled when you want all promises to finish regardless of their outcome. Promise.race is useful when you need only the result of the fastest promise.

Chaining Promises

Chaining promises allows for a sequence of asynchronous operations to be performed one after another while keeping the code flat and readable.

1new Promise(function(resolve, reject) {
2    setTimeout(() => resolve(1), 1000); // (*)
3})
4.then(function(result) { // (**)
5    console.log(result); // 1
6    return result * 2;
7})
8.then(function(result) { // (***)
9    console.log(result); // 2
10    return result * 2;
11})
12.then(function(result) {
13    console.log(result); // 4
14    return result * 2;
15});

Best Practice: Always return results in .then to keep the chain going. Use .catch at the end of the chain to handle any errors that may occur in the chain.

Async/Await: Syntactic Sugar over Promises

async/await is syntactic sugar built on top of Promises. It allows you to write asynchronous code that looks and behaves a bit more like synchronous code, which makes it easier to understand and maintain.

1async function fetchData() {
2    let response = await fetch('https://jsonplaceholder.typicode.com/users');
3    let data = await response.json();
4    console.log(data);
5}
6
7fetchData();

Error Handling in Async/Await

Error handling in async/await is done using try/catch blocks, making it similar to synchronous code’s error handling.

1async function fetchData() {
2    try {
3        let response = await fetch('https://jsonplaceholder.typicode.com/users');
4        let data = await response.json();
5        console.log(data);
6    } catch (error) {
7        console.error('Error fetching data:', error);
8    }
9}
10
11fetchData();

Best Practice: Always use try/catch blocks in async functions to handle errors. It ensures that asynchronous code behaves similarly to synchronous code, making it easier to manage.

Conclusion

Asynchronous JavaScript, through callbacks, promises, and async/await, provides powerful tools for handling operations that don’t complete immediately. Mastering these concepts allows for writing more robust, performant, and maintainable JavaScript code, especially in environments where I/O operations are prevalent.
As you grow more familiar with these patterns, you’ll be better equipped to handle the complexities of asynchronous operations in your web projects. For advanced topics in asynchronous JavaScript, consider exploring patterns like async iterators or advanced async/await patterns.

Happy coding!

asynchronous javascriptasynchronousasync-awaitjavascript promisescallback-hell
Share:

Subscribe to newsletter

Get all the latest posts delivered straight to your inbox.