Bạn đã bao giờ ‘lạc lối’ trong mớ code .then() lồng nhau khi làm việc với JavaScript Promises? Cú pháp async await javascript chính là vị cứu tinh, giúp bạn viết code bất đồng bộ gọn gàng và dễ đọc như code đồng bộ. Trong hướng dẫn toàn diện này, WiWeb sẽ cùng bạn tìm hiểu từ A-Z cách sử dụng async/await để xử lý các tác vụ phức tạp một cách đơn giản và hiệu quả nhất.
Async Await JavaScript là gì?
Trong lĩnh vực lập trình asynchronous programming của JavaScript, việc xử lý các tác vụ tốn thời gian như gọi API hay đọc file là chuyện thường ngày. Trước đây, chúng ta thường dùng Promises với chuỗi .then(). Tuy nhiên, cách viết này có thể trở nên phức tạp. Đây chính là lúc async/await tỏa sáng.
Giải mã ‘Syntactic Sugar’: Cách async/await làm code Promises dễ đọc hơn
Bạn có thể hiểu async/await là một lớp “vỏ bọc” cú pháp đẹp mắt cho JavaScript Promises. Nó không phải là một tính năng hoàn toàn mới mà là một cách viết khác, giúp code của bạn trông gọn gàng hơn rất nhiều. Thay vì lồng các hàm vào nhau, bạn có thể viết code theo một luồng thẳng, tuần tự. Điều này giúp việc đọc, hiểu và bảo trì code trở nên dễ dàng hơn bao giờ hết.
Mục đích chính của async await: Viết code bất đồng bộ như code đồng bộ
Đây chính là siêu năng lực của async/await. Nó cho phép bạn viết các đoạn code bất đồng bộ (phải chờ đợi) nhưng nhìn qua lại giống hệt như code đồng bộ (chạy từ trên xuống dưới). Não của chúng ta vốn quen với tư duy tuần tự, vì vậy cách viết này cực kỳ trực quan và thân thiện.
Sự khác biệt cốt lõi giữa từ khóa ‘async’ và ‘await’
Hai từ khóa này luôn đi đôi với nhau nhưng có nhiệm vụ riêng biệt:
- async: Khi bạn đặt từ khóa này trước một hàm, bạn đang “báo hiệu” cho JavaScript rằng: “Hàm này là một async function và nó sẽ luôn trả về một Promise”. Kể cả khi bạn
returnmột giá trị thông thường (như số 5), JavaScript sẽ tự động bọc nó trong một Promise đã được giải quyết (resolved promise). - await: Từ khóa này chỉ có thể được sử dụng bên trong một hàm
async. Nó ra lệnh cho JavaScript: “Hãy tạm dừng việc thực thi hàm này tại đây. Chờ cho đến khi Promise ở phía sau từ khóaawaithoàn thành rồi mới đi tiếp”.

Cú pháp và cách hoạt động của Async/Await
Hiểu được khái niệm rồi, giờ chúng ta cùng xem cách triển khai chúng trong thực tế nhé. Cú pháp của async/await rất đơn giản và dễ nhớ.
Khai báo một hàm bất đồng bộ với từ khóa ‘async’
Để tạo một hàm bất đồng bộ, bạn chỉ cần thêm từ khóa async vào trước khai báo hàm. Rất đơn giản phải không?
// Khai báo một async function thông thường
async function layDuLieu() {
return "Đây là dữ liệu";
}
// Hoặc với arrow function
const layDuLieuArrow = async () => {
return "Dữ liệu từ arrow function";
};
Khi bạn gọi hàm layDuLieu(), kết quả trả về sẽ không phải là chuỗi "Đây là dữ liệu" ngay lập tức. Thay vào đó, nó sẽ là một Promise chứa giá trị đó.
Tạm dừng thực thi hàm với từ khóa ‘await’
Từ khóa await là nơi phép màu xảy ra. Nó được đặt trước một Promise (ví dụ như kết quả từ một lệnh fetch). Khi JavaScript gặp await, nó sẽ tạm dừng hàm async hiện tại và chờ đợi Promise đó được giải quyết.
async function hienThiDuLieu() {
console.log("Bắt đầu lấy dữ liệu...");
// Giả sử fetchData() là một hàm trả về Promise
const duLieu = await fetchData(); // <-- Hàm sẽ dừng ở đây cho đến khi fetchData() hoàn thành
console.log(duLieu); // Dòng này chỉ chạy sau khi có dữ liệu
console.log("Đã lấy dữ liệu xong!");
}
Lưu ý quan trọng: Không thể sử dụng ‘await’ bên ngoài hàm ‘async’
Đây là một quy tắc bắt buộc bạn phải nhớ. Nếu bạn cố gắng sử dụng await ở phạm vi toàn cục hoặc trong một hàm thông thường (không có async), JavaScript sẽ báo lỗi SyntaxError. Lý do là vì await cần một môi trường bất đồng bộ (do async tạo ra) để có thể “tạm dừng” và “tiếp tục” mà không làm chặn luồng chính của chương trình.

So sánh Async/Await và Promises: Cách viết nào tốt hơn?
Cả hai cách viết đều dùng để xử lý tác vụ bất đồng bộ. Vậy đâu là lựa chọn tối ưu hơn? Hãy xem một so sánh trực tiếp để thấy rõ sự khác biệt.
Từ Promise Chaining (.then().catch()) đến Async/Await
Hãy tưởng tượng chúng ta có một tác vụ: lấy thông tin người dùng, sau đó dùng ID người dùng để lấy danh sách bài viết của họ.
Cách viết dùng Promise .then():
function layBaiVietCuaNguoiDung() {
fetch('https://api.example.com/user/1')
.then(response => response.json())
.then(user => {
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => {
console.log(posts);
})
.catch(error => {
console.error('Đã xảy ra lỗi:', error);
});
}
Cách viết dùng Async/Await:
async function layBaiVietCuaNguoiDungAsync() {
try {
const userResponse = await fetch('https://api.example.com/user/1');
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
console.log(posts);
} catch (error) {
console.error('Đã xảy ra lỗi:', error);
}
}
Ưu điểm vượt trội về tính dễ đọc và bảo trì code
Nhìn vào hai ví dụ trên, bạn có thể thấy ngay phiên bản async/await trông sạch sẽ và dễ theo dõi hơn hẳn. Nó đọc gần giống như một câu chuyện tuần tự: lấy người dùng, chờ, rồi lấy bài viết, chờ, rồi hiển thị. Cách viết này loại bỏ hoàn toàn các khối .then() lồng nhau, giúp giảm thiểu rủi ro tạo ra “Promise Hell” và giúp việc gỡ lỗi (debug) dễ dàng hơn nhiều.
Bản chất: Async/Await được xây dựng trên nền tảng Promises
Điều quan trọng cần nhấn mạnh là async/await không thay thế Promises. Nó chỉ là một cách viết khác, tiện lợi hơn, được xây dựng trên nền tảng của Promises. Mọi hàm async đều trả về một Promise. Mọi thứ bạn có thể làm với Promises (như Promise.all hay Promise.race), bạn cũng có thể làm với async/await.

Xử lý lỗi (Error Handling) trong Async/Await một cách hiệu quả
Một trong những ưu điểm lớn nhất của async/await là việc xử lý lỗi trở nên cực kỳ trực quan. Bạn không cần đến .catch() ở cuối chuỗi Promise nữa.
Sử dụng khối try…catch quen thuộc cho code bất đồng bộ
Với async/await, bạn có thể sử dụng khối try…catch kinh điển, giống hệt như khi bạn xử lý lỗi trong code đồng bộ. Đây là một cú pháp mà hầu hết các lập trình viên đều đã quen thuộc.
Logic hoạt động rất đơn giản:
- try: Đặt toàn bộ đoạn code bất đồng bộ (các lệnh
await) mà bạn nghĩ có thể phát sinh lỗi vào trong khối này. - catch: Nếu bất kỳ Promise nào trong khối
trybị từ chối (rejected), việc thực thi sẽ ngay lập tức nhảy đến khốicatch, mang theo thông tin lỗi.
Cách tiếp cận này giúp tập trung toàn bộ logic xử lý lỗi vào một chỗ, làm cho code của bạn gọn gàng và dễ quản lý hơn.
Ví dụ: Bắt lỗi khi gọi API (fetch) thất bại
Hãy xem một ví dụ thực tế về error handling async await. Chúng ta sẽ cố gắng gọi đến một API không tồn tại và xem khối try catch javascript hoạt động như thế nào.
async function getDataFromInvalidAPI() {
try {
console.log('Đang cố gắng gọi API...');
const response = await fetch('https://api.example.com/khong-ton-tai');
// Kiểm tra nếu response không thành công (vd: lỗi 404, 500)
if (!response.ok) {
throw new Error(`Lỗi HTTP! Trạng thái: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
// Bất kỳ lỗi nào (lỗi mạng, lỗi HTTP,...) đều sẽ được bắt ở đây
console.error('Không thể lấy dữ liệu:', error.message);
}
}
getDataFromInvalidAPI();
Trong ví dụ này, lệnh fetch có thể thất bại do lỗi mạng, hoặc response.ok trả về false. Cả hai trường hợp đều sẽ khiến luồng thực thi nhảy vào khối catch, giúp bạn xử lý sự cố một cách mượt mà.

Ví dụ thực tế: Lấy dữ liệu từ API bằng Async/Await và Fetch
Lý thuyết là vậy, giờ hãy cùng xem một ví dụ hoàn chỉnh và thực tế nhất: sử dụng fetch để lấy danh sách người dùng từ một API công khai.
Code mẫu gọi API đơn giản
Chúng ta sẽ sử dụng JSONPlaceholder, một API miễn phí tuyệt vời để thử nghiệm. Đây là một fetch api example kinh điển.
// Hàm để lấy và hiển thị danh sách người dùng
async function fetchUsers() {
const apiUrl = 'https://jsonplaceholder.typicode.com/users';
try {
// 1. Gửi yêu cầu và chờ đợi phản hồi
const response = await fetch(apiUrl);
// 2. Kiểm tra xem yêu cầu có thành công không
if (!response.ok) {
throw new Error(`Lỗi mạng: ${response.status}`);
}
// 3. Chuyển đổi phản hồi sang định dạng JSON và chờ đợi
const users = await response.json();
// 4. In ra tên của người dùng đầu tiên
console.log('Lấy dữ liệu thành công!');
console.log('Người dùng đầu tiên:', users[0].name);
} catch (error) {
// 5. Bắt và xử lý bất kỳ lỗi nào xảy ra trong quá trình
console.error('Đã có lỗi xảy ra:', error.message);
}
}
// Gọi hàm để thực thi
fetchUsers();
Phân tích chi tiết từng bước hoạt động của code
Bạn có thấy code trên dễ đọc như thế nào không? Hãy cùng phân tích từng bước:
- Dòng
const response = await fetch(apiUrl);gửi một yêu cầu mạng. Chương trình sẽ tạm dừng ở đây cho đến khi máy chủ phản hồi. - Chúng ta kiểm tra thuộc tính
response.ok. Nếu có lỗi từ phía server (như 404 Not Found), chúng ta chủ động tạo ra một lỗi để khốicatchxử lý. - Dòng
const users = await response.json();lấy nội dung của phản hồi. Đây cũng là một tác vụ bất đồng bộ, nên chúng ta cầnawaitđể chờ nó phân tích xong dữ liệu JSON. - Sau khi có dữ liệu
users, chúng ta có thể làm việc với nó như một mảng JavaScript thông thường. - Nếu có bất kỳ sự cố nào ở bước 1, 2, hoặc 3, luồng thực thi sẽ nhảy thẳng vào khối
catchvà in ra thông báo lỗi.

Các kỹ thuật nâng cao và những điều cần lưu ý
Khi đã nắm vững những điều cơ bản, bạn có thể tìm hiểu một vài kỹ thuật nâng cao để tối ưu hóa code của mình hơn nữa.
Thực thi song song nhiều tác vụ với Promise.all và await
Giả sử bạn cần lấy thông tin người dùng và danh sách sản phẩm từ hai API khác nhau. Nếu bạn await từng cái một, tác vụ thứ hai sẽ phải chờ tác vụ đầu tiên xong. Điều này không hiệu quả.
// Cách không hiệu quả (tuần tự)
async function fetchDataSequentially() {
const user = await fetch('.../user/1').then(res => res.json());
const products = await fetch('.../products').then(res => res.json());
}
Thay vào đó, chúng ta có thể kết hợp await với Promise.all. Promise.all nhận vào một mảng các Promise và trả về một Promise duy nhất. Promise này sẽ hoàn thành khi tất cả các Promise trong mảng đều hoàn thành.
// Cách hiệu quả (song song)
async function fetchDataInParallel() {
try {
const [user, products] = await Promise.all([
fetch('.../user/1').then(res => res.json()),
fetch('.../products').then(res => res.json())
]);
console.log('Thông tin người dùng:', user);
console.log('Danh sách sản phẩm:', products);
} catch (error) {
console.error('Lỗi khi lấy dữ liệu song song:', error);
}
}
Cách này giúp tiết kiệm thời gian đáng kể vì các yêu cầu mạng được thực hiện đồng thời.
Khi nào không nên sử dụng async await?
Dù rất mạnh mẽ, không phải lúc nào async/await cũng là lựa chọn tốt nhất. Trong một số trường hợp đơn giản, việc sử dụng .then() có thể ngắn gọn hơn. Ví dụ, khi bạn chỉ cần thực hiện một hành động đơn lẻ sau khi Promise hoàn thành và không cần kết quả của nó cho các bước tiếp theo.
// Dùng .then() có thể ngắn gọn hơn ở đây
fetchData().then(() => {
console.log('Tác vụ đã hoàn thành!');
});
Quan trọng là hiểu rõ cả hai cách viết và chọn cách phù hợp nhất cho từng tình huống cụ thể.

Lời kết
Qua hướng dẫn này, WiWeb hy vọng bạn đã có một cái nhìn toàn diện và sâu sắc về async await javascript. Đây không chỉ là một cú pháp mới, mà là một công cụ mạnh mẽ giúp bạn viết code bất đồng bộ sạch sẽ, dễ đọc và dễ bảo trì hơn rất nhiều. Từ việc hiểu rõ async và await, xử lý lỗi với try...catch, cho đến các kỹ thuật nâng cao như Promise.all, bạn đã có đủ hành trang để tự tin áp dụng vào các dự án của mình.
Bạn đã áp dụng async await javascript vào dự án nào của mình chưa? Hãy chia sẻ kinh nghiệm hoặc câu hỏi của bạn ở phần bình luận nhé!
Nếu bạn đang tìm kiếm một đối tác để xây dựng website chuyên nghiệp với code hiện đại và tối ưu, WiWeb luôn sẵn sàng đồng hành. Liên hệ với chúng tôi để được tư vấn nhé!


Khi mình thử dùng `await` bên trong vòng lặp `forEach` để gọi API cho một danh sách thì kết quả không tuần tự như mong đợi, ad có gợi ý cách xử lý cho trường hợp này không?
Cảm ơn câu hỏi rất hay của bạn, đây là một trường hợp rất thường gặp! Vòng lặp `forEach` không “chờ” `await` đâu bạn, nên để xử lý tuần tự, bạn hãy thử thay bằng vòng lặp `for…of` nhé, nó sẽ hoạt động đúng như bạn mong đợi.