Async-Await + Promise.All + Callback
Using async/await
and Promises in frontend React applications and backend services.
Use Case: Fetching Data from MongoDB
Let's say we have a React frontend that needs to fetch a list of users from a MongoDB database. We'll compare the use of async/await
and Promises for both the frontend and backend.
Backend (MongoDB)
Using async/await
async/await
// api/getUsers.js
const { MongoClient } = require('mongodb');
const uri = process.env.MONGO_URI;
let client;
async function connectToDatabase() {
if (!client) {
client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
await client.connect();
}
return client.db('mydatabase');
}
module.exports = async (req, res) => {
try {
const db = await connectToDatabase();
const users = await db.collection('users').find({}).toArray();
res.status(200).json(users);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' });
}
};
Using Promises
// api/getUsers.js
const { MongoClient } = require('mongodb');
const uri = process.env.MONGO_URI;
let client;
function connectToDatabase() {
if (!client) {
client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
return client.connect().then(() => client.db('mydatabase'));
}
return Promise.resolve(client.db('mydatabase'));
}
module.exports = (req, res) => {
connectToDatabase()
.then((db) => db.collection('users').find({}).toArray())
.then((users) => res.status(200).json(users))
.catch((error) => res.status(500).json({ error: 'Failed to fetch users' }));
};
Frontend (React)
Using async/await
// components/UserList.js
import React, { useState, useEffect } from 'react';
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('/api/getUsers');
const data = await response.json();
setUsers(data);
} catch (error) {
console.error('Failed to fetch users', error);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) {
return <p>Loading...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user._id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
Using Promises
// components/UserList.js
import React, { useState, useEffect } from 'react';
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/getUsers')
.then((response) => response.json())
.then((data) => {
setUsers(data);
setLoading(false);
})
.catch((error) => {
console.error('Failed to fetch users', error);
setLoading(false);
});
}, []);
if (loading) {
return <p>Loading...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user._id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
Comparison
Readability
async/await
: Generally easier to read and understand, especially for those familiar with synchronous code. It makes the code look cleaner and less nested, which improves maintainability.Promises: Can become harder to read and maintain due to nested
.then
and.catch
blocks, especially in complex scenarios.
Error Handling
async/await
: Usingtry/catch
blocks allows for more straightforward error handling.Promises: Error handling with
.catch
is effective but can lead to nested code, making it harder to follow.
Debugging
async/await
: Easier to debug since it appears more synchronous, and stack traces are more straightforward.Promises: Can be harder to debug due to the asynchronous nature and chaining, which can make stack traces more complex.
Performance
Both: Performance-wise, there is no significant difference between
async/await
and Promises. Both are built on top of the same underlying mechanisms.
Conclusion
While both approaches are valid, async/await
tends to offer better readability and maintainability, especially as the complexity of your code increases. For simple use cases, Promises might be sufficient, but for more complex logic and error handling, async/await
is generally preferred.
Sure! Promise.all
is useful when you need to perform multiple asynchronous operations concurrently and wait for all of them to complete before proceeding. Let's extend our use case to demonstrate this. Assume that in addition to fetching users, we also need to fetch posts and comments, and we want to display all this information together in our React component.
Backend (MongoDB)
First, we’ll set up our backend to handle fetching users, posts, and comments.
Using async/await
with Promise.all
async/await
with Promise.all
// api/getData.js
const { MongoClient } = require('mongodb');
const uri = process.env.MONGO_URI;
let client;
async function connectToDatabase() {
if (!client) {
client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
await client.connect();
}
return client.db('mydatabase');
}
module.exports = async (req, res) => {
try {
const db = await connectToDatabase();
const [users, posts, comments] = await Promise.all([
db.collection('users').find({}).toArray(),
db.collection('posts').find({}).toArray(),
db.collection('comments').find({}).toArray(),
]);
res.status(200).json({ users, posts, comments });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch data' });
}
};
Frontend (React)
Now, we’ll update our React component to fetch and display users, posts, and comments together.
Using async/await
with Promise.all
// components/DataList.js
import React, { useState, useEffect } from 'react';
const DataList = () => {
const [data, setData] = useState({ users: [], posts: [], comments: [] });
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/getData');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Failed to fetch data', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return <p>Loading...</p>;
}
return (
<div>
<h2>Users</h2>
<ul>
{data.users.map((user) => (
<li key={user._id}>{user.name}</li>
))}
</ul>
<h2>Posts</h2>
<ul>
{data.posts.map((post) => (
<li key={post._id}>{post.title}</li>
))}
</ul>
<h2>Comments</h2>
<ul>
{data.comments.map((comment) => (
<li key={comment._id}>{comment.text}</li>
))}
</ul>
</div>
);
};
export default DataList;
What is callback?
A callback is a function that is passed as an argument to another function and is executed after some event or operation has completed. Callbacks are used to handle asynchronous operations in JavaScript, allowing you to continue executing code while waiting for the asynchronous operation to finish.
Example of Callbacks
Here's a basic example to illustrate how callbacks work:
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'John Doe' };
callback(data);
}, 1000); // Simulating an asynchronous operation with setTimeout
}
function handleData(data) {
console.log('Data received:', data);
}
fetchData(handleData);
In this example:
fetchData
is a function that takes a callback function as an argument.Inside
fetchData
, we simulate an asynchronous operation usingsetTimeout
.After the operation completes (after 1 second), the
callback
function is called with thedata
.
Callbacks:
Readability: Can become difficult to read and maintain, especially with nested callbacks, leading to "callback hell".
Error Handling: Each callback needs to handle errors individually, which can scatter error handling logic.
Flow Control: Managing the flow of multiple asynchronous operations can become complex.
Promises:
Readability: Easier to read and manage than callbacks, especially with chaining.
Error Handling: Centralized error handling using
.catch()
.Flow Control: More straightforward than callbacks, but can still become complex with deeply nested
.then()
chains.
async/await
:
Readability: The most readable and cleanest approach, resembling synchronous code.
Error Handling: Centralized error handling using
try/catch
.Flow Control: Simplifies the flow of asynchronous operations, making it easier to follow.
What to do if I want to hit multiple Api's ?
When you need to hit multiple APIs concurrently, you can use Promise.all
with async/await
. Promise.all
allows you to execute multiple asynchronous operations in parallel and wait for all of them to complete. Here's how you can achieve this:
Example: Fetching Data from Multiple APIs
Assume you need to fetch user data, post data, and comment data from three different API endpoints.
Backend (Node.js with async/await
and Promise.all
)
javascriptCopy code// api/getData.js
const fetch = require('node-fetch'); // or use axios
const API_USER = 'https://api.example.com/users';
const API_POST = 'https://api.example.com/posts';
const API_COMMENT = 'https://api.example.com/comments';
module.exports = async (req, res) => {
try {
const [usersResponse, postsResponse, commentsResponse] = await Promise.all([
fetch(API_USER),
fetch(API_POST),
fetch(API_COMMENT),
]);
if (!usersResponse.ok || !postsResponse.ok || !commentsResponse.ok) {
throw new Error('Failed to fetch data from one or more APIs');
}
const [users, posts, comments] = await Promise.all([
usersResponse.json(),
postsResponse.json(),
commentsResponse.json(),
]);
res.status(200).json({ users, posts, comments });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch data' });
}
};
Frontend (React Component with async/await
and Promise.all
)
javascriptCopy code// components/DataList.js
import React, { useState, useEffect } from 'react';
const DataList = () => {
const [data, setData] = useState({ users: [], posts: [], comments: [] });
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/getData');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Failed to fetch data', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return <p>Loading...</p>;
}
return (
<div>
<h2>Users</h2>
<ul>
{data.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<h2>Posts</h2>
<ul>
{data.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<h2>Comments</h2>
<ul>
{data.comments.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
</div>
);
};
export default DataList;
How It Works
Backend
Fetching Data Concurrently:
The
fetch
function is used to make HTTP requests to different API endpoints.Promise.all
is used to send requests to all three APIs concurrently.The first
Promise.all
waits for all fetch requests to complete, and then we check if all responses are successful.The second
Promise.all
converts the responses to JSON concurrently.
Error Handling:
If any of the fetch requests fail, an error is thrown and caught by the
catch
block, returning a 500 status with an error message.
Frontend
Fetching Combined Data:
The
fetchData
function is called inside auseEffect
hook to fetch data from the backend when the component mounts.The data received from the backend API is stored in the state and used to render the UI.
Loading state management ensures the UI displays a loading message until all data is fetched and ready.
Benefits of Using Promise.all
Promise.all
Concurrency: Executes multiple asynchronous operations in parallel, making the process faster than executing them sequentially.
Error Handling: If any of the promises reject,
Promise.all
immediately rejects with that reason, making it easier to handle errors.Simplified Code: Using
Promise.all
withasync/await
keeps the code clean and readable, avoiding deeply nested callbacks or multiple.then
chains.
Conclusion
By leveraging async/await
with Promise.all
, you can efficiently handle multiple concurrent asynchronous operations. This approach is particularly useful when fetching data from multiple APIs, ensuring your application remains performant and your codebase stays maintainable.
Last updated