JavaScript is single-threaded. Async programming allows JS to handle long running tasks without blocking the main execution thread, ensuring a responsive user experience.

Callbacks

Callbacks are functions passed as arguments to other functions and are executed after the completion of a task.

Callbacks were the original way to handle async operations in JavaScript but are now considered outdated, resulting in “callback hell.”

myDiv.addEventListener("click", function() {
  // execute callback
});

Promises

A promise represents a value that may be available now, in the future, or never. Promises provide a cleaner and more robust way to handle async operations.

Promise states
  • Pending: Initial state, neither fulfilled or rejected.
  • Fulfilled: Operation completed successfully.
  • Rejected: Operation failed.
function fetchUser(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
	    if (isValid(userId)) {
	      const fetchedUser = ...;
	      resolve(fetchedUser);
	    } else {
	      reject(new Error('User not found.'));
	    }
    }, 1000);
  });
};
fetchUser('123')
  .then((user) => { /* handle result */ })
  .catch((error) => { /* handle error */ })
  .finally(res => { /* called regardless of success or failure */ });
Handling multiple promises

Promise.all allows you to wait for multiple promises to resolve, returning a single promise.

Promise.all([promise1, promise2])
  .then((result) => { /* [result 1, result 2] */ })

Promise.race returns a promise that resolves or ejects as soon as one of the promises in the array resolves or rejects.

Promise.race([promise1, promise2])
  .then((result) => { /* Result of first resolved promise */ })

async/await

async/await are syntactic sugar over promises, making async code read more like synchronous code. (Really. The async function actually returns a promise. Returning from an async function resolves the promise. Throwing an error rejects the promise.)

async declares an asynchronous function that returns a promise. await pauses the execution of an async function until the promise is resolved or rejected.

See the difference below, both code blocks do the exact same thing:

// fetch using promise syntax (without async/await)
function getPersonsInfo(name) {
  return server.getPeople().then(people => {
    return people.find(person => { return person.name === name });
  });
}
// fetch using async/await syntax
async function getPersonsInfo(name) {
  const people = await server.getPeople();
  return people.find(person => { return person.name === name });
}

Error handling

Use try…catch blocks to handle errors in async/await.

async function asyncFetch(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    return data;
  } catch (err) {
    // handle error
  }
}

Since async functions return a promise, you can handle errors at their call site by appending .catch(), like any other promise.

asyncFetch(url)
  .then(data => { /* handle data */ })
  .catch(err =>. { /* handle error */ });