At some point in development you may find yourself using a callback. There’s nothing wrong with that, but then you realise you need another callback to fire after the first callback, and so on. This is referred to as ‘Callback Hell!’

Callbacks are a convention of JavaScript and are the same as any other function. The difference is not what's inside them, but when they’re used.

Callbacks are typically used after a task that takes time to complete. JavaScript is generally synchronous. It will complete one task before moving onto the next, but developers aren't keen on waiting for tasks with unknown completion times.

What happens if the task doesn’t even finish and throws an error half way through the process? This is where the use of Promises and Asynchronous programming come in.

Promises

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value. A Promise is used to overcome issues with multiple callbacks and provides a better way to manage success and error conditions.

A Promise object has 3 states:

  • Pending: The async operation is going on
  • Resolved: The async operation is completed successfully
  • Rejected: The async operation is completed with error

Usually the Promise would wrap around an AJAX call or any other asynchronous code. But for this example the Math.random() is used to test both resolved and rejected results.

   const myPromise = new Promise((resolve, reject) = {
    if (Math.random() * 100  50) { // usually would be an asychronous operation
        console.log('resolving the promise ...');
        resolve('resolved!');
    }
    reject('rejected!');
});

console.log(myPromise);
// output if resolved: 'resolved!'
// output if rejected: 'rejected!'

There's no need to write in an else block because the Promise stops as soon as the first condition is returned to the object.

Enjoying this article? Sign up to our newsletter

Developers can chain Promises using .then() and .catch(). The .then() method accepts two callbacks. The first callback is invoked when the Promise is resolved. The second callback is executed when the Promise is rejected.

   const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 100 > 50) { // usually would be an asychronous operation
        console.log('resolving the promise ...');
        resolve('resolved!');
    }
    reject('rejected!');
});

const onResolved = (resolvedValue) => console.log(resolvedValue);
const onRejected = (rejectedValue) => console.log(rejectedValue);

myPromise.then(onResolved).catch(onRejected);
// output if resolved: 'resolved!'
// output if rejected: 'rejected!'

As a way of error handling the .catch() method can be used. It returns a Promise object and deals with rejected results only.

   myPromise.catch((error) => console.log(error));

If there’s a scenario where the fastest result is needed, regardless of whether the Promise is resolved or rejected, use the .race() method. The .race() method uses an iterable as its parameter such as an array.

   const promise1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
    console.log(value); // output: 'two'
});

If there are multiple Promises the .all() method can be used for them to finish. This method takes an array.

   const promise1 = new Promise ((resolve, reject) => {
	resolve('promise 1 has been resolved');
});

const promise2 = new Promise ((resolve, reject) => {
	resolve('promise 2 has been resolved');
});

const promise3 = new Promise ((resolve, reject) => {
	resolve('promise 3 has been resolved');
});

Promise.all([promise1, promise2, promise3]).then((message) => {
	console.log(message);
});

// output: ["promise 1 has been resolved", "promise 2 has been resolved", "promise 3 has been resolved"]

Async and await

Think of async and await as syntactic sugar wrapped around Promises to make them easier to work with. Below is an example of chaining Promises:

   function checkLanguage(language) {
  return new Promise((resolve, reject) => {
      if (language === 'css') { // usually would be an asychronous operation
          resolve(language);
      }
      reject(`${language} is not used to style websites`);
    });
}

function languageConfirm(response) {
    return new Promise((resolve, reject) => {
        resolve(`${response} is used to style websites`);
    });
}

checkLanguage('css').then(checkResponse => { // checkResponse = 'css'
    return languageConfirm(checkResponse ); // will return 'css is used to style websites'
}).then(confirmResponse => { // confirmResponse = 'css is used to style websites'
    console.log(confirmResponse ); // output: 'css is used to style websites' 
}).catch(error => {
	console.log(error);
});

checkLanguage('html').then(checkResponse => { // checkResponse = 'html'
    return languageConfirm(checkResponse );
}).then(confirmResponse => {
    console.log(confirmResponse );
}).catch(error => { // error = 'html is not used to style websites'
    console.log(error); // output: 'html is not used to style websites'
});

The first invocation of checkLanguage chains resolves and allows the chain to continue. The second invocation of checkLanguagepromise gets rejected and the error is caught and outputed in the console. It’s possible to keep chaining by using .then() but this would end up being untidy.

Instead, the chaining can be re-written:

   function checkLanguage(language) {
  return new Promise((resolve, reject) => {
      if (language === 'css') { // usually would be an asychronous operation
          resolve(language);
      }
      reject(`${language} is not used to style websites`);
    });
}

function languageConfirm(response) {
  return new Promise((resolve, reject) => {
        resolve(`${response} is used to style websites`);
  });
}

async function myAsyncFunction() {
  try {
      const responseA = await checkLanguage('css');
      console.log(responseA); // output: 'css'
      const responseB = await languageConfirm(responseA);
      console.log(responseB); // output: 'css is used to style websites'
  } catch (error) {
      console.log(error);
  }
};

myAsyncFunction();

Summary

  • Using async and await is easier to reason with
  • It looks like synchronous code so it’s more readable and understandable
  • Async await makes it easier to write and work with promises
  • Await ensures it returns the result of the promise being executed
  • Promises and async/await accomplish the same thing. They make retrieving and handling asynchronous data easier. They eliminate the need for callbacks, simplify error handling, reduce extraneous code, make waiting for multiple concurrent calls to return and adding additional code in between calls easier

Find out more about faster async functions and Promises.

Our front-end development team are constantly looking at ways to improve the development process the user experience. We work on exciting design and build projects often on CMS like Sitecore.