Introduction to Promises and async/await in JavaScript: Understanding the Basics of Asynchronous Programming in JavaScript
A Promise object is used for handling asynchronous computations that have some important guarantees difficult to handle with the callback method. A Promise object is simply a wrapper around a value that may or may not be known when the object is instantiated and provides a method for handling the value after it is known (also known as resolved) or is unavailable for a failure reason (we'll refer to this as rejected).
Using a Promise object gives us the opportunity to associate functionality for an asynchronous operation's eventual success or failure. It also allows us to treat these complex scenarios by using synchronous-like code.
A promise only ever has one of three states at any given time: pending, fulfilled (resolved), or rejected (error). A pending promise can only ever lead to either a fulfilled state or a rejected state once and only once, which can avoid some pretty complex error scenarios. This means that we can only ever return a promise once. If we want to rerun a function that uses promises, we need to create a new one.
We can create new promises using the Promise constructor, which accepts a function that will get run with two parameters: the onSuccess (or resolve) function to be called on success resolution and the onFail (or reject) function to be called on failure rejection.
Defining Promises: What are Promises and How do They Work?
A Promise object is used for handling asynchronous computations that have some important guarantees that are difficult to handle with the callback method. A Promise object is simply a wrapper around a value that may or may not be known when the object is instantiated and provides a method for handling the value after it is known (also referred to as resolved) or is unavailable for a failure reason (we'll refer to this as rejected).
Using a Promise object gives us the opportunity to associate functionality for an asynchronous operation's eventual success or failure, allowing us to treat these complex scenarios by using synchronous-like code. A promise only ever has one of three states at any given time: pending, fulfilled (resolved), or rejected (error). A pending promise can only ever lead to either a fulfilled state or a rejected state once and only once, which can avoid some pretty complex error scenarios.
We can create new promises using the Promise constructor. It accepts a function that will get run with two parameters: the onSuccess (or resolve) function to be called on success resolution and the onFail (or reject) function to be called on failure rejection. For instance:
var promise = new Promise(function(resolve, reject) {
// call resolve if the method succeeds
resolve(true);
});
promise.then(bool => console.log('Bool is true'));
This allows us to handle asynchronous operations in a more straightforward way, making it easier to manage complex scenarios.
The Problem with Callback Hell: Why Promises Were Created
In the past, handling asynchronous computations in JavaScript was a nightmare. This is because callbacks were used to handle these operations. However, this led to a phenomenon known as "callback hell." When you're working with nested callbacks, it's easy to get lost in a sea of code and struggle to understand what's happening.
Promises were created to solve this problem. A promise represents an operation that hasn't completed yet but will eventually produce a value or throw an error. This allows us to write asynchronous code that looks and feels like synchronous code.
Here's an example of how promises can simplify your code:
function getCurrentTime(onSuccess, onFail) {
// Get the current 'global' time from an API using Promise
return new Promise((resolve, reject) => {
setTimeout(function() {
var didSucceed = Math.random() >= 0.5;
didSucceed ? resolve(new Date()) : reject('Error');
}, 2000);
});
}
getCurrentTime()
.then(currentTime => getCurrentTime())
.then(currentTime => {
console.log('The current time is: ' + currentTime);
return true;
})
.catch(err => console.log('There was an error:' + err));
In this example, we create a promise that represents the operation of getting the current time. We then use the then method to handle the success and failure cases of the promise. This allows us to write code that's easy to read and understand, without having to deal with the complexity of nested callbacks.
Assembling Promises: Creating and Working with Promise Chains
A Promise object is used for handling asynchronous computations, providing important guarantees that are difficult to handle with the callback method. A Promise object is simply a wrapper around a value that may or may not be known when the object is instantiated and provides a method for handling the value after it is known (also known as resolved) or is unavailable for a failure reason (we'll refer to this as rejected).
Using a Promise object gives us the opportunity to associate functionality for an asynchronous operation's eventual success or failure, allowing us to treat these complex scenarios by using synchronous-like code.
Here's an example of creating and working with promises:
return new Promise((resolve, reject) => {
setTimeout(function() {
var didSucceed = Math.random() >= 0.5;
didSucceed ? resolve(new Date()) : reject('Error');
}, 2000);
})
In this example, we create a promise that resolves after 2 seconds with the current time if Math.random() is greater than or equal to 0.5, otherwise it rejects with an error message.
Assembling promises involves creating new promises and chaining them together using the then() method. This allows us to handle the value of one promise as the input for another promise.
Introducing async/await: A Higher-Level Syntax for Handling Asynchronous Code
Using a Promise object gives us the opportunity to associate functionality for an asynchronous operation's eventual success or failure. It also allows us to treat these complex scenarios by using synchronous-like code.
For instance, consider the following synchronous code where we print out the current time in the JavaScript console:
var currentTime = new Date(); console.log('The current time is: ' + currentTime);
This is pretty straight-forward and works as the new Date() object represents the time the browser knows about. Now consider that we're using a different clock on some other remote machine. Using promises, however, helps us avoid a lot of this complexity.
The previous code, which could be called spaghetti code, can be turned into a neater, more synchronous-looking version:
function getCurrentTime(onSuccess, onFail) {
// Get the current 'global' time from an API using Promise
return new Promise((resolve, reject) => {
setTimeout(function() {
var didSucceed = Math.random() >= 0.5;
didSucceed ? resolve(new Date()) : reject('Error');
}, 2000);
});
}
getCurrentTime().then(currentTime => {
console.log('The current time is: ' + currentTime);
return true;
}).catch(err => console.log('There was an error:' + err));
This previous example is a bit cleaner and clear as to what's going on, avoiding a lot of tricky error handling/catching.
How async/await Works: Understanding the Magic Behind the Scenes
async/await is built on top of promises and allows you to write asynchronous code that looks synchronous. When you call an async function, it returns a promise. The await keyword then waits for that promise to resolve or reject before moving on to the next line of code.
When you use await on a promise, it's actually calling the then() method on that promise and waiting for the result. If the promise resolves, await will return the value of the promise. If the promise rejects, await will throw an error.
Here's a simple example:
function getCurrentTime() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Date());
}, 2000);
});
}
async function logCurrentTime() {
try {
const currentTime = await getCurrentTime();
console.log('The current time is: ' + currentTime);
} catch (err) {
console.log('There was an error: ' + err);
}
}
logCurrentTime();
In this example, the getCurrentTime() function returns a promise that resolves after 2 seconds with the current date and time. The logCurrentTime() function uses await to wait for the promise to resolve before logging the result. If there's an error, it will be caught by the catch block and logged to the console.
Practical Application of async/await in JavaScript: Building a Simple API Request
In this function startTimer() makes a POST request to the /api/timers/start endpoint. The server needs the id of the timer and the start time. That request method looks like:
function startTimer(data) {
return fetch('/api/timers/start', {
method: 'post',
body: JSON.stringify(data),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}).then(checkStatus);
}
In addition to headers, the request parameters object that we pass to fetch() has two more properties:
This API call is made using the fetch API which returns a promise. Let's consider what a Promise object is and how it helps us handle asynchronous operations.
Common Patterns and Use Cases for async/await in JavaScript
When working with asynchronous operations in JavaScript, it's common to encounter scenarios where you need to handle multiple promises or async/await operations. Here are some common patterns and use cases for using async/await:
One common pattern is to use async/await to chain together multiple promises. For example, consider a scenario where you're making multiple API requests to fetch data. You can use async/await to write the code in a synchronous-looking way:
async function fetchData() {
const response1 = await fetch('/api/data1');
const response2 = await fetch('/api/data2');
const response3 = await fetch('/api/data3');
// Process the responses as needed
}
Another pattern is to use async/await to handle errors in a more straightforward way. For example, consider a scenario where you're making an API request and need to catch any errors that might occur:
async function fetchData() {
try {
const response = await fetch('/api/data');
// Process the response as needed
} catch (error) {
console.error('Error:', error);
}
}
Finally, async/await can be used to simplify code that involves recursive functions or infinite loops. For example:
async function fibonacci(n) {
if (n <= 1) return n;
return await Promise.resolve(fibonacci(n - 1)) + await Promise.resolve(fibonacci(n - 2));
}
console.log(await fibonacci(10)); // Output: 55
These are just a few examples of the many use cases for async/await in JavaScript. By using these patterns, you can write more readable and maintainable code that's easier to understand and debug.
Conclusion: What You've Learned About Promises and async/await in JavaScript
What You've Learned About Promises and async/await in JavaScript
You now have a solid understanding of promises and how they can be used to handle asynchronous code. Remember that a Promise object is a wrapper around a value that may or may not be known when the object is instantiated, providing a method for handling the value after it is known (resolved) or unavailable for a failure reason (rejected). This allows you to associate functionality with an asynchronous operation's eventual success or failure, and treat these complex scenarios using synchronous-like code.
You've also seen how promises can help avoid complexity in your code. For instance, consider the following example where we print out the current time:
function getCurrentTime(onSuccess, onFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
var didSucceed = Math.random() >= 0.5;
didSucceed ? resolve(new Date()) : reject('Error');
}, 2000);
});
}
getCurrentTime()
.then(currentTime => getCurrentTime())
.then(currentTime => {
console.log('The current time is: ' + currentTime);
return true;
})
.catch(err => console.log('There was an error:' + err));
This code uses promises to handle asynchronous operations, making it easier to read and maintain.