JavaScript Asynchronous: Promise và Async/Await từ A-Z (Part 2)
Frontend Dzui Lắm
Tác giả

Ở Part 1, chúng ta đã tìm hiểu:
JavaScript Asynchronous.
Event Loop.
Promise.
Async/Await.
Promise vs Async/Await.
Trong Part 2, chúng ta sẽ đi vào một bài toán rất phổ biến khi làm Frontend:
Làm thế nào để xử lý nhiều API request cùng lúc một cách hiệu quả?
Đây là vấn đề xuất hiện rất thường xuyên trong:
React.
Angular.
Vue.
Next.js.
Node.js.
Dashboard.
E-commerce.
Admin system.
1. Sequential Request
Giả sử Dashboard cần gọi 3 API:
GET /users
GET /products
GET /orders
Cách viết đơn giản:
const users = await getUsers();
const products = await getProducts();
const orders = await getOrders();
Flow:
getUsers()
↓
done
↓
getProducts()
↓
done
↓
getOrders()
↓
done
Nếu mỗi request mất khoảng 2 giây:
2s + 2s + 2s = 6s
Nhưng có một câu hỏi quan trọng:
getProducts()có thực sự cần chờgetUsers()không?
Nếu không thì chúng ta đang lãng phí thời gian.
2. Concurrent Request
Nếu 3 API độc lập:
getUsers()
getProducts()
getOrders()
chúng ta có thể khởi chạy chúng gần như đồng thời.
┌── getUsers()
│
Start ───────┼── getProducts()
│
└── getOrders()
Nếu:
Users → 2s
Products → 2s
Orders → 2s
thì tổng thời gian có thể gần:
≈ 2s
thay vì:
≈ 6s
Đây chính là lợi ích của concurrent request.
3. Promise.all()
Cách phổ biến nhất là:
const [users, products, orders] = await Promise.all([
getUsers(),
getProducts(),
getOrders()
]);
Có thể hình dung:
getUsers() ───────┐
│
getProducts() ───────┼── Promise.all()
│
getOrders() ───────┘
Tất cả Promise được tạo trước.
Sau đó Promise.all() chờ chúng hoàn thành.
4. Promise.all() trả về kết quả theo đúng thứ tự
Giả sử:
const [users, products, orders] = await Promise.all([
getUsers(),
getProducts(),
getOrders()
]);
Thời gian thực tế:
users → 3s
products → 1s
orders → 2s
Mặc dù products hoàn thành đầu tiên, kết quả vẫn:
users
products
orders
theo đúng thứ tự Promise được truyền vào.
Đây là điểm rất quan trọng khi destructuring kết quả.
5. Promise.all() và Error
Promise.all() có một behavior quan trọng:
Chỉ cần một Promise reject, Promise.all cũng reject.
Ví dụ:
try {
const [users, products, orders] = await Promise.all([
getUsers(),
getProducts(),
getOrders()
]);
} catch (error) {
console.error(error);
}
Nếu:
Users → SUCCESS
Products → ERROR
Orders → SUCCESS
thì:
Promise.all()
↓
REJECT
Điều này phù hợp khi tất cả API đều quan trọng.
6. Promise.allSettled()
Nếu chúng ta muốn:
Dù API nào fail thì vẫn chờ tất cả API hoàn thành.
Sử dụng:
const results = await Promise.allSettled([
getUsers(),
getProducts(),
getOrders()
]);
Kết quả:
[
{
status: "fulfilled",
value: users
},
{
status: "rejected",
reason: error
},
{
status: "fulfilled",
value: orders
}
]
Đây là lựa chọn rất tốt cho Dashboard.
Ví dụ:
Dashboard
User Statistics ✓
Revenue ✓
Orders ✓
Notifications ✗
Nếu Notifications lỗi, chúng ta vẫn có thể render các phần còn lại.
7. Promise.race()
Promise.race() trả về Promise đầu tiên settle.
Ví dụ:
const result = await Promise.race([
requestA(),
requestB()
]);
Nếu:
Request A → 5s
Request B → 2s
thì Request B hoàn thành trước và quyết định kết quả.
Một use case phổ biến là timeout.
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Request timeout"));
}, 5000);
});
const response = await Promise.race([
fetch("/api/users"),
timeout
]);
Nếu request mất hơn 5 giây, timeout sẽ reject.
8. Promise.any()
Promise.any() khác Promise.race() ở một điểm quan trọng:
Nó chỉ quan tâm Promise đầu tiên fulfilled.
Ví dụ:
const result = await Promise.any([
requestServerA(),
requestServerB(),
requestServerC()
]);
Nếu:
Server A → ERROR
Server B → ERROR
Server C → SUCCESS
thì kết quả Server C được trả về.
Đây là pattern hữu ích cho hệ thống có nhiều nguồn dữ liệu dự phòng.
9. So sánh 4 Promise Combinators
MethodBehaviorPromise.all()Tất cả thành côngPromise.allSettled()Chờ tất cả, kể cả thất bạiPromise.race()Promise settle đầu tiênPromise.any()Promise fulfilled đầu tiên
Có thể nhớ:
all
→ Tất cả
allSettled
→ Tất cả, bất kể success/error
race
→ Ai về trước
any
→ Ai thành công trước
10. Promise.all() với map()
Một use case cực kỳ phổ biến:
const userIds = [1, 2, 3, 4, 5];
Muốn lấy detail của tất cả users.
Không nên:
const users = userIds.map(async id => {
return await getUserDetail(id);
});
Bởi vì kết quả là:
[
Promise,
Promise,
Promise,
Promise,
Promise
]
Cách đúng:
const users = await Promise.all(
userIds.map(id => getUserDetail(id))
);
Bây giờ:
users
là dữ liệu thực tế.
Đây là pattern nên nhớ:
Array
↓
map()
↓
Promise[]
↓
Promise.all()
↓
Result[]
11. Vì sao await trong for loop có thể chậm?
Ví dụ:
for (const id of userIds) {
const user = await getUserDetail(id);
console.log(user);
}
Nếu có 5 users:
User 1
↓
User 2
↓
User 3
↓
User 4
↓
User 5
Các request chạy tuần tự.
Nếu mỗi request mất 1 giây:
≈ 5 giây
Nếu các request độc lập:
const users = await Promise.all(
userIds.map(id => getUserDetail(id))
);
thời gian có thể giảm đáng kể.
12. Khi nào await tuần tự lại đúng?
Không phải lúc nào parallel cũng tốt.
Ví dụ:
Get User
↓
Get user.id
↓
Get Orders
↓
Get order.id
↓
Get Order Detail
Request sau phụ thuộc request trước.
Code:
const user = await getUser();
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
Ở đây chạy tuần tự là bắt buộc.
Không thể chuyển toàn bộ thành:
Promise.all([
getUser(),
getOrders(),
getOrderDetail()
]);
vì chúng ta chưa có dữ liệu cần thiết cho request sau.
13. Kết hợp Sequential + Parallel
Đây là pattern rất thường gặp trong dự án thực tế.
Ví dụ:
Get User
↓
userId
↓
┌──────────────┬────────────────┬────────────────┐
│ │ │
Orders Notifications Profile
│ │ │
└──────────────┴────────────────┴────────────────┘
↓
Dashboard
Code:
const user = await getUser();
const [
orders,
notifications,
profile
] = await Promise.all([
getOrders(user.id),
getNotifications(user.id),
getProfile(user.id)
]);
Đây thường là cách tối ưu hơn so với:
const user = await getUser();
const orders = await getOrders(user.id);
const notifications = await getNotifications(user.id);
const profile = await getProfile(user.id);
14. Đừng Promise.all() hàng nghìn request
Giả sử chúng ta có:
const ids = [...Array(10000)];
Nếu viết:
await Promise.all(
ids.map(id => getUserDetail(id))
);
chúng ta có thể tạo ra 10.000 request gần như cùng lúc.
Đây có thể gây:
API rate limit.
Server overload.
Network congestion.
Browser resource exhaustion.
Nhiều request fail.
Tăng latency.
Vì vậy:
Parallel request cần đi cùng với concurrency control.
15. Batch Processing
Một cách đơn giản là chia request thành từng batch.
Ví dụ:
const batchSize = 10;
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const results = await Promise.all(
batch.map(id => getUserDetail(id))
);
console.log(results);
}
Flow:
100 requests
Batch 1
10 requests
↓
Batch 2
10 requests
↓
Batch 3
10 requests
↓
...
Thay vì:
100 requests
████████████████████
chúng ta giới hạn:
10 requests
██████████
10 requests
██████████
10 requests
██████████
16. Concurrency Limit
Batch processing là cách đơn giản để giới hạn số request.
Trong các ứng dụng phức tạp hơn, chúng ta có thể sử dụng concurrency limit.
Ví dụ:
Maximum concurrency = 5
Nghĩa là tại một thời điểm chỉ có tối đa:
5 requests
đang chạy.
Khi một request hoàn thành:
Request 1 ✓
slot được giải phóng và request tiếp theo bắt đầu:
Request 6 → START
Flow:
1 2 3 4 5
██████████
Request 3 done
1 2 4 5 6
██████████
Đây là một concept rất quan trọng khi xử lý large-scale asynchronous tasks.
17. Error Handling khi xử lý nhiều Request
Một pattern:
async function loadDashboard() {
try {
const [
users,
products,
orders
] = await Promise.all([
getUsers(),
getProducts(),
getOrders()
]);
return {
users,
products,
orders
};
} catch (error) {
console.error("Failed to load dashboard", error);
}
}
Nếu tất cả API đều critical thì cách này phù hợp.
Nếu API độc lập và một API fail không nên ảnh hưởng các API khác:
const results = await Promise.allSettled([
getUsers(),
getProducts(),
getOrders()
]);
Sau đó xử lý từng result.
18. Case Study: Dashboard
Giả sử Dashboard cần:
User
Revenue
Orders
Products
Notifications
Một implementation chưa tối ưu:
const user = await getUser();
const revenue = await getRevenue();
const orders = await getOrders();
const products = await getProducts();
const notifications = await getNotifications();
Nếu mỗi API mất 1 giây:
≈ 5 giây
Nếu các API không phụ thuộc nhau:
const [
user,
revenue,
orders,
products,
notifications
] = await Promise.all([
getUser(),
getRevenue(),
getOrders(),
getProducts(),
getNotifications()
]);
Thời gian lý thuyết có thể gần:
≈ max(
user,
revenue,
orders,
products,
notifications
)
Nếu mỗi API khoảng 1 giây:
≈ 1 giây
Thay vì:
≈ 5 giây
19. Cách tư duy khi gặp bài toán nhiều API
Khi gặp một flow có nhiều request, hãy đặt 4 câu hỏi.
1. Request có phụ thuộc nhau không?
Nếu có:
Sequential
Nếu không:
Parallel
2. Tất cả request có bắt buộc thành công không?
Nếu có:
Promise.all()
Nếu không:
Promise.allSettled()
3. Số lượng request có lớn không?
Nếu nhỏ:
Promise.all()
Nếu rất lớn:
Batch
Concurrency Limit
Pagination
4. Request nào quan trọng?
Có thể chia:
Critical API
↓
Must succeed
Optional API
↓
Can fail independently
Điều này giúp thiết kế error handling tốt hơn.
20. Best Practices
Không await tuần tự một cách vô thức
Không nên:
const a = await getA();
const b = await getB();
const c = await getC();
nếu A, B, C hoàn toàn độc lập.
Thay vào đó:
const [a, b, c] = await Promise.all([
getA(),
getB(),
getC()
]);
Không Promise.all() vô hạn
Không nên:
Promise.all(
thousandsOfItems.map(...)
);
nếu API/server có giới hạn.
Hãy cân nhắc:
Batch
Concurrency Limit
Pagination
Không dùng Promise.all() cho các request có dependency
Ví dụ:
User
↓
User ID
↓
Orders
thì phải sequential.
Chọn Promise combinator theo business requirement
Need all success?
→ Promise.all()
Need every result?
→ Promise.allSettled()
Need first settled?
→ Promise.race()
Need first successful?
→ Promise.any()
21. Tổng kết
Khi xử lý asynchronous trong JavaScript, điều quan trọng không phải là:
"Có nên dùng async/await hay Promise?"
Mà quan trọng hơn là:
Các asynchronous operation có dependency với nhau hay không?
Nếu có:
A
↓
B
↓
C
Sử dụng:
await
theo tuần tự.
Nếu không:
A ──┐
B ──┼── Promise.all()
C ──┘
Nếu có hàng nghìn request:
Many Requests
↓
Concurrency Control
↓
Batch / Limit
Và cuối cùng:
Promise.all()
↓
Tất cả thành công
Promise.allSettled()
↓
Quan tâm tất cả kết quả
Promise.race()
↓
Ai settle trước
Promise.any()
↓
Ai success trước
Đây là cách tư duy quan trọng khi thiết kế asynchronous flow trong các ứng dụng Frontend hiện đại.
Không phải cứ chạy nhiều request cùng lúc là tốt.
Mục tiêu thực sự là:
Chạy các task độc lập song song, giữ các task có dependency tuần tự, đồng thời kiểm soát concurrency để cân bằng giữa performance và resource usage.
Thích bài viết này?
Nội dung trên Vết Mực luôn được chia sẻ miễn phí. Nếu bài viết mang lại giá trị cho bạn, hãy cân nhắc ủng hộ để chúng mình có thể duy trì máy chủ, phát triển thêm tính năng mới và tiếp tục xây dựng một không gian dành cho những người yêu viết lách. ✨
Các cách ủng hộ:
- •Viết và đăng bài trên Vết Mực
- •Chia sẻ bài viết với bạn bè
- •Góp ý để chúng mình cải thiện sản phẩm qua email: nsikhoa@gmail.com
Dù bạn chọn ủng hộ hay chỉ đơn giản là tiếp tục đọc và chia sẻ bài viết, đó đều là nguồn động lực rất lớn với chúng mình. ❤️
Bình luận
Đăng nhập để để lại bình luận.