Class 13: Asynchronous Programming (Promises and async/await)
In the previous class, we introduced asynchronous programming
with setTimeout
and setInterval
to
handle tasks without blocking the main thread. Today, we delve
into more advanced and powerful patterns for managing
asynchronous operations: Promises and the
modern Async/Await syntax, which are essential
for handling network requests and other time-consuming
operations in a clean and efficient manner.
Understanding Callback Hell and the Need for Better Patterns
Before Promises, deeply nested callbacks were common for handling sequential asynchronous operations, leading to what's known as "callback hell" or "pyramid of doom." This code becomes hard to read, maintain, and debug.
// Example of Callback Hell (illustrative - not runnable)
fetchUser(function(user) {
fetchUserPosts(user.id, function(posts) {
fetchPostComments(posts[0].id, function(comments) {
console.log("User, posts, and comments fetched:", { user, posts, comments });
}, function(err) {
console.error("Error fetching comments:", err);
});
}, function(err) {
console.error("Error fetching posts:", err);
});
}, function(err) {
console.error("Error fetching user:", err);
});
Promises were introduced to provide a more structured and readable way to handle asynchronous operations, especially when they are chained together.
Promises
A Promise
is an object representing the eventual
completion or failure of an asynchronous operation. A Promise
can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected.
- Fulfilled (Resolved): The operation completed successfully.
- Rejected: The operation failed.
// Creating a simple Promise
const myPromise = new Promise((resolve, reject) => {
// Simulate an asynchronous operation (e.g., fetching data)
const success = true; // Change to false to see the 'catch' block
setTimeout(() => {
if (success) {
resolve("Data fetched successfully!"); // Operation succeeded
} else {
reject("Failed to fetch data."); // Operation failed
}
}, 1000);
});
// Consuming the Promise
myPromise.then((message) => {
console.log("Success:", message); // Runs if the promise is fulfilled
}).catch((error) => {
console.error("Error:", error); // Runs if the promise is rejected
}).finally(() => {
console.log("Promise operation finished."); // Runs regardless of success or failure
});
console.log("Promise initiated..."); // This runs first
/* Expected Output (if success is true):
Promise initiated...
(after 1 second)
Success: Data fetched successfully!
Promise operation finished.
*/
/* Expected Output (if success is false):
Promise initiated...
(after 1 second)
Error: Failed to fetch data.
Promise operation finished.
*/
Chaining Promises (.then()
)
Promises can be chained to perform a sequence of asynchronous
operations. Each .then()
block returns a new
Promise, allowing you to chain the next operation.
function fetchData(message, delay) {
return new Promise(resolve => {
setTimeout(() => {
console.log(message);
resolve(message); // Resolve with the message for the next chain
}, delay);
});
}
fetchData("Step 1: Fetching user data...", 1000)
.then(data => fetchData("Step 2: Processing user data...", 800))
.then(data => fetchData("Step 3: Fetching user posts...", 1200))
.then(data => console.log("All steps completed."))
.catch(error => console.error("An error occurred in the chain:", error));
/* Expected Output (with delays):
Step 1: Fetching user data...
Step 2: Processing user data...
Step 3: Fetching user posts...
All steps completed.
*/
Using the fetch
API for HTTP Requests
The fetch()
API provides a modern, Promise-based
interface for making network requests (e.g., to fetch data from
a server, submit form data). It replaces the older
XMLHttpRequest
.
Basic GET Request with fetch
// Fetch data from a public API (e.g., JSONPlaceholder)
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => {
// Check if the request was successful (status code 2xx)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json(); // Parse the JSON response body
})
.then(data => {
console.log("Fetched post:", data);
console.log("Title:", data.title);
})
.catch(error => {
console.error("Error fetching data:", error);
});
console.log("Request sent..."); // This runs first
/* Expected Output:
Request sent...
(after network delay)
Fetched post: { userId: 1, id: 1, title: 'sunt aut...', body: 'quia et ...' }
Title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
*/
-
fetch()
returns aResponse
object, which is itself a Promise. -
.then(response => response.json())
is common to parse the response body as JSON. -
Always include a
.catch()
to handle network errors or errors from the server.
POST Request with fetch
const newPost = {
title: 'foo',
body: 'bar',
userId: 1,
};
fetch("https://jsonplaceholder.typicode.com/posts", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost), // Convert JavaScript object to JSON string
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log("New post created:", data);
})
.catch(error => {
console.error("Error creating post:", error);
});
Simplifying Async Code with Async/Await
async/await
is a modern JavaScript syntax
(introduced in ES2017) built on top of Promises that makes
asynchronous code look and behave more like synchronous code,
making it much easier to read and write.
-
The
async
keyword is used to define an asynchronous function. Anasync
function implicitly returns a Promise. -
The
await
keyword can only be used inside anasync
function. It pauses the execution of theasync
function until the Promise it's waiting for settles (resolves or rejects).
async function getPostTitle(postId) {
try {
// await pauses execution until the fetch Promise resolves
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// await pauses execution until the .json() Promise resolves
const data = await response.json();
console.log(`Post ${postId} title:`, data.title);
return data.title;
} catch (error) {
console.error("Failed to get post title:", error);
return null;
}
}
console.log("Calling getPostTitle...");
getPostTitle(2);
getPostTitle(9999); // Will likely cause an error (e.g., 404 Not Found)
console.log("Function call initiated...");
/* Expected Output:
Calling getPostTitle...
Function call initiated...
(after network delay for post 2)
Post 2 title: qui est esse inventore
(after network delay for post 9999)
Failed to get post title: Error: HTTP error! status: 404
*/
-
try...catch
blocks are used to handle errors inasync/await
functions, similar to synchronous error handling. -
await
can only be used directly insideasync
functions. If you need to useawait
outside anasync
function (e.g., at the top level of a module), you might wrap it in an Immediately Invoked Async Function Expression (IIAFE) or use a modern environment that supports top-level await.