A Beginner's Journey into Asynchronous JavaScript: Promises and Async/Await Explained 🤞🔥
Hello developers, If you’ve ever worked with JavaScript, you probably know that it’s a language that supports asynchronous programming. This means that you can write code that doesn’t block the execution of other code and can handle multiple tasks at the same time.
But how do you write asynchronous code in JavaScript? And how do you manage the complexity and avoid the pitfalls of callback hell?
In this blog post, I’ll explain two concepts that can help you write cleaner and more readable asynchronous code in JavaScript: promises and async-await.
What are promises?🤔
A promise is an object that represents the result of an asynchronous operation. It can be in one of three states:
Pending: The operation is not yet completed, and the promise is waiting for the result.
Fulfilled: The operation is completed successfully, and the promise has value.
Rejected: The operation is completed with an error, and the promise has a reason.
You can think of a promise as a placeholder for a future value. It’s like saying “I promise to give you something later, but I don’t know what it is or when it will be ready”.
For example, let’s say you want to order a pizza online. You fill out the form, click the order button, and wait for the confirmation. The confirmation is a promise: it tells you that your order has been received, but it doesn’t tell you when your pizza will be delivered or if there will be any issues.
Inside the Promise: States and Execution Flow🧐
A promise has two main components: an executor function and a handler function.
The executor function is the function that creates the promise and performs the asynchronous operation. It has two parameters: resolve
and reject
. These are functions that you can use to either fulfill or reject the promise.
The handler functions are the functions that handle the result of the promise. They are attached to the promise using the .then()
and .catch()
methods. The .then()
method takes two functions as arguments: one for the fulfillment case, and one for the rejection case. The .catch()
method takes one function as an argument, which handles any rejection that occurs in the promise chain.
Creating a Promise🔬
You can create a promise using the new Promise()
constructor, which takes an executor function as an argument. This function is called immediately when the promise is created, and it has access to the resolve
and reject
functions.
For example, let’s say we want to create a promise that simulates a network request. We can use the setTimeout()
function to mimic a delay, and then call either resolve
or reject
depending on some conditions.
const promise = new Promise((resolve, reject) => {
// Perform asynchronous task here
// If successful, call resolve(result)
// If there's an error, call reject(error)
});
Resolving and Rejecting Promises:
To resolve a Promise with a value, call the
resolve
function inside the executor. If there's an error or the operation fails, call thereject
function with the appropriate error.
const fetchUserData = () => {
return new Promise((resolve, reject) => {
fetch('https://api.example.com/users')
.then((response) => response.json())
.then((data) => resolve(data))
.catch((error) => reject(error));
});
};
Handling Promise Results:
We can handle the results of a Promise using the
then()
method, which is called when the Promise is resolved successfully. To handle errors, use thecatch()
method.
fetchUserData()
.then((data) => {
console.log('User data:', data);
})
.catch((error) => {
console.error('Error fetching user data:', error);
});
Chaining Promises:
Promises can be chained using multiple then()
calls. This is useful when you need to perform sequential asynchronous operations.
const promise1 = new Promise((resolve, reject) => {
// Do something asynchronous
resolve('The first promise is resolved!');
});
const promise2 = new Promise((resolve, reject) => {
// Do something asynchronous
resolve('The second promise is resolved!');
});
const promise3 = new Promise((resolve, reject) => {
// Do something asynchronous
resolve('The third promise is resolved!');
});
promise1
.then(() => promise2)
.then(() => promise3)
.then((value) => {
// Do something with the value of the third promise
});
When this code is executed, the first promise is resolved. The then()
method on the first promise then calls the then()
method on the second promise, passing in the value that the first promise was resolved with. The then()
method on the second promise then calls the then()
method on the third promise, passing in the value that the second promise was resolved with. Finally, the then()
method on the third promise calls the callback function that was passed in, passing in the value that the third promise was resolved with.
fetchUserData()
.then((data) => {
// Process the user data and return some value
return processUserData(data);
})
.then((processedData) => {
// Further process the data or make another API call
return someOtherTask(processedData);
})
.then((finalResult) => {
console.log('Final result:', finalResult);
})
.catch((error) => {
console.error('Error:', error);
});
Understanding Async/Await in JavaScript
What is Async/Await?
Async/await is a new syntax in JavaScript that makes it easier to write asynchronous code. Async/await allows you to write code that looks like synchronous code, but that actually runs asynchronously.
How Async/Await Works?
To use async/await, a function must be marked with the async
keyword. Inside an async function, you can use the await
keyword to pause the function execution until a Promise is resolved. This makes the code appear more synchronous, improving its readability.
async function getData() {
try {
const result = await someAsyncFunction();
console.log('Result:', result);
} catch (error) {
console.error('Error:', error);
}
}
Error Handling with Async/Await:
We use the try...catch
block to handle errors in async/await functions. If a Promise is rejected or an error occurs, the catch block will handle it gracefully.
async function getData() {
try {
const result = await someAsyncFunction();
console.log('Result:', result);
} catch (error) {
console.error('Error:', error);
}
}
Combining Async/Await with Promise Chaining:
Async/await can be used in combination with Promise chaining to perform complex asynchronous operations in a more structured way.
async function processUser() {
try {
const userData = await fetchUserData();
const processedData = await processUserData(userData);
const finalResult = await someOtherTask(processedData);
console.log('Final result:', finalResult);
} catch (error) {
console.error('Error:', error);
}
}
The processUser()
function is an async function that utilizes the power of await
to handle asynchronous operations in a synchronous-like manner. It demonstrates the use of await
with multiple asynchronous functions to process user data step by step. Let's break down the code and understand how it works:
async function processUser() {
- The function is declared as an
async
function, indicating that it contains asynchronous operations and will return a Promise implicitly.
- The function is declared as an
try {
- We start a
try
block to handle potential errors that might occur during the asynchronous operations.
- We start a
const userData = await fetchUserData();
- The first
await
statement pauses the execution ofprocessUser()
until the Promise returned byfetchUserData()
is resolved or rejected. Once the Promise is resolved, the result (user data) is stored in the variableuserData
.
- The first
const processedData = await processUserData(userData);
- The second
await
statement pauses the execution again until the Promise returned byprocessUserData(userData)
is resolved or rejected. TheuserData
obtained from the previous step is passed as an argument. The result of this operation is stored in the variableprocessedData
.
- The second
const finalResult = await someOtherTask(processedData);
- The third
await
statement pauses the execution again until the Promise returned bysomeOtherTask(processedData)
is resolved or rejected. TheprocessedData
obtained from the previous step is passed as an argument. The result of this operation is stored in the variablefinalResult
.
- The third
console.log('Final result:', finalResult);
- After all the asynchronous operations are successfully completed, the final result is logged to the console.
} catch (error) {
- If any error occurs during the
await
operations within thetry
block, the execution jumps to the correspondingcatch
block.
- If any error occurs during the
console.error('Error:', error);
- The
catch
block is executed when there's an error in any of the awaited operations. The error is logged to the console usingconsole.error()
.
- The
By using await
, we avoid the need to nest multiple .then()
calls, which can lead to callback hell. Instead, the code is organized in a more linear and readable manner, making it easier to understand the flow of asynchronous operations. The try...catch
block ensures that any errors occurring during the asynchronous tasks are handled gracefully without crashing the program.
Remember that to use await
, the containing function must be marked as async
. Async functions always return a Promise, either explicitly or implicitly. In this case, processUser()
implicitly returns a Promise that resolves when all the asynchronous operations inside it are completed, or rejects if any of the awaited Promises reject.
Conclusion:
Promises and Async/Await are powerful tools in JavaScript for dealing with asynchronous operations. Promises simplify handling asynchronous tasks by providing a structured approach to managing their resolution and rejection. On the other hand, Async/Await takes it a step further by making the syntax more concise and synchronous-looking.
Choose the approach that suits your coding style and project requirements.
Fuel Your Passion! 🚀 Discover new horizons in our upcoming blog!