Promises with Async/Await

Promises are one approach to asynchronous programming. A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. Promises have three states: pending, resolved, or rejected.Functions which perform asynchronous operations (such as interacting with the file system or a database) can return a promise which is then resolved later. If that promise can't be kept (something is rejected) we can catch the error and deal with it.

Then and Catch

getDataFromFile()
// promise is resolved
.then((result) => {
doSomethingWithData(result)
})
// promise is rejected
.catch((err) => {
handleError(err)
})

This example is about as simple as it gets. We don't need to understand the exact details of how to return a promise in order to use getDataFromFile. We just need to expect that:

  1. The then function will call doSomethingWithData if there is no error. doSomethingWithData can be passed a value.

  2. The catch function will call handleError if there is an error, and handleError will be passed an error (often a JavaScript Error object).

If getDataFromFile returns a resolved promise, then will be called (and catch won't be). If it returns a rejected promise, catch will be called (and then won't be).

If you are familiar with callbacks, you'll notice it's like a callback that has had the err and the data handling parts separated. The same thing might be written like this with callbacks:

getDataFromFileCallback(function(err, data) {
if (err) {
console.error(err.message)
} else {
console.log(data)
}
})

Promise Chains

We can also string together quite long 'promise chains' which define the order certain tasks should occur in:

getDataFromFile()
.then((uncheckedMessyData) => {
return checkTheData(uncheckedMessyData)
})
.then((messyData) => {
return modifyTheData(messyData)
})
.then((tidyData) => {
displayTheData(tidyData)
})
.catch((err) => {
handleError(err)
})

So long as each function in the chain returns a data object, this will help ensure everything takes place in the correct order. For example, displayTheData always gets called after modifyTheData.

Async/Await

The async/await syntax is a more concise way to work with promises. It was introduced in ECMAScript 2017 (ES8) to simplify the process of writing asynchronous code, making it look more like synchronous code.

Here's the original example from above using async/await:

const fetchDataAndDoSomething = async () => {
try {
// promise is resolved
const data = await getDataFromFile()
doSomethingWithData(data)
}
catch err {
// promise is rejected
console.log(err)
}
}

In the async/await example:

  • The fetchDataAndDoSomething function is marked with the async keyword, indicating that it returns a promise.
  • Inside the function, the await keyword is used to pause execution until the promise is resolved. This makes the asynchronous code look more like synchronous code.
  • The try/catch block is used for error handling, making it more readable and manageable.

You can also chain promises with async/await. Here's the chained promise code from above written with async/await:

const processDataFromFile = async () => {
try {
const uncheckedMessyData = await getDataFromFile();
const messyData = await checkTheData(uncheckedMessyData);
const tidyData = await modifyTheData(messyData);
displayTheData(tidyData);
} catch (err) {
handleError(err);
}
}

In this refactored code:

  • Each .then() block is replaced with an await statement, making the code more linear and easier to read.
  • The try/catch block is used to handle errors, similar to the .catch() at the end of the original code.
  • The await keyword is used to wait for each asynchronous operation to complete before moving on to the next step in the process.

In summary, async/await is a syntactic sugar built on top of promises, providing a cleaner and more readable way to work with asynchronous code. It simplifies the process of handling promises, especially when dealing with multiple asynchronous operations or when chaining promises.