Có bao giờ bạn dành cả buổi chiều để debug một con bug “trời ơi đất hỡi”, để rồi nhận ra mình quên await một Promise, hoặc tệ hơn là quên bọc nó trong một khối try...catch chưa? Mình thì rồi, không chỉ một lần. Xử lý lỗi trong JavaScript, đặc biệt với code bất đồng bộ, đôi khi giống như đi trên dây vậy. Tại Phạm Hải, sau nhiều năm “chinh chiến” với các dự án lớn nhỏ, mình nhận ra rằng việc nắm vững xử lý lỗi JavaScript try catch best practices là ranh giới giữa một ứng dụng chập chờn và một hệ thống vững như bàn thạch. Nếu bạn là người mới đang loay hoay với nền tảng, việc tham khảo Học JavaScript cơ bản cho người mới 2026 là bước đệm tuyệt vời trước khi đào sâu vào bài viết này. Bài viết này là kinh nghiệm xương máu của mình, chia sẻ những chiến thuật để bạn không chỉ bắt được lỗi, mà còn quản lý chúng một cách chuyên nghiệp nhất.
Async/Await và try...catch: Cặp đôi hoàn hảo để xử lý lỗi bất đồng bộ
Kết hợp try...catch với cú pháp Async/Await syntax chính là cách xử lý lỗi async await hiệu quả nhất hiện nay, giúp biến những đoạn code bất đồng bộ phức tạp trở nên dễ đọc và tuyến tính như code đồng bộ.
Trước đây, khi làm việc với Promise, chúng ta thường xuyên rơi vào cảnh “callback hell” hoặc phải nối chuỗi .catch() method dài dằng dặc. Việc xử lý lỗi đồng bộ bất đồng bộ JavaScript lúc đó thực sự là một cơn ác mộng vì luồng code nhảy lung tung. Nhưng từ khi async/await ra đời, mọi thứ đã thay đổi. Thay vì tách biệt luồng xử lý lỗi, bạn có thể gom tất cả vào một khối try...catch duy nhất.
Nhiều bạn hay thắc mắc xử lý lỗi async await có tốt hơn promise không? Câu trả lời là về mặt logic thì tương đương, nhưng về mặt “sạch sẽ” và dễ bảo trì thì async/await ăn đứt. Tất nhiên, để hiểu sâu xa tại sao lại như vậy, việc nắm vững nền tảng qua bài viết Async Await Promise JavaScript dễ hiểu là vô cùng cần thiết.
Tại sao try...catch “bó tay” với Promise nếu thiếu await?
Nếu bạn quên từ khóa await trước một hàm bất đồng bộ, khối try...catch sẽ thực thi xong và thoát ra trước khi Promise kịp trả về lỗi (Promise rejection), dẫn đến tình trạng try catch async await không hoạt động.
Rất nhiều bạn Junior gặp phải lỗi này. Bạn gọi một API, bọc nó trong try...catch, nhưng ứng dụng vẫn crash với lỗi UnhandledPromiseRejectionWarning. Lý do là JavaScript không chờ Promise đó giải quyết (Promise resolution) mà đã chạy qua mất rồi. Vậy khi nào nên dùng try catch với async await? Đó là khi bạn chắc chắn đã đặt await trước các tác vụ gọi mạng hoặc truy vấn database.
// ❌ SAI: try...catch không bắt được lỗi vì thiếu await
try {
fetchData(); // Trả về Promise nhưng không có await
} catch (error) {
console.log("Sẽ không bao giờ in ra dòng này nếu fetchData bị lỗi");
}
// ✅ ĐÚNG:
try {
await fetchData(); // Đợi Promise giải quyết xong
} catch (error) {
console.log("Bắt lỗi thành công!");
}
Code-along: Xử lý lỗi API call với fetch một cách sạch sẽ và an toàn.
Để xử lý lỗi Fetch API JavaScript đúng chuẩn, bạn bắt buộc phải kiểm tra response.ok hoặc status code vì fetch mặc định không ném lỗi (throw error) với các HTTP errors như 404 hay 500.
Một trong những best practices xử lý lỗi HTTP JavaScript mà mình luôn dặn team tại Phạm Hải là đừng bao giờ tin tưởng hoàn toàn vào khối catch khi dùng Fetch API. Khối catch của fetch chỉ nhảy vào khi có lỗi mạng (mất mạng, không resolve được DNS). Nếu server trả về lỗi 500 Internal Server Error, fetch vẫn coi đó là một request thành công! Để thực hành tốt phần này, bạn có thể xem thêm hướng dẫn chi tiết về Fetch API gọi REST API bằng JavaScript.
Dưới đây là mẫu code chuẩn mực mà mình thường dùng, thay vì phải cài thêm thư viện bên thứ ba như Axios:
async function getUserData() {
try {
const response = await fetch('https://api.example.com/user');
// Bắt buộc phải check response.ok với fetch
if (!response.ok) {
// Sử dụng throw keyword để ném ra một Error object kèm status
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Lỗi khi gọi API:", error.message);
// Xử lý fallback UI ở đây
}
}
Đừng bao giờ quên finally: Cách dọn dẹp “bãi chiến trường” sau khi try...catch kết thúc.
Khối finally block luôn được thực thi bất kể code trong try thành công hay thất bại, rất lý tưởng để dọn dẹp tài nguyên như tắt loading spinner hoặc đóng kết nối database.
Khi sử dụng cú pháp try catch finally JavaScript, bạn sẽ tránh được việc lặp lại code một cách dư thừa. Ví dụ, bạn bật biến isLoading = true trước khi gọi API. Thay vì phải set isLoading = false ở cả trong try và catch, bạn chỉ cần vứt nó vào finally. Điều này giúp việc quản lý lỗi trong ứng dụng JavaScript gọn gàng và ít rủi ro quên logic hơn rất nhiều.
Không chỉ có try...catch: Những chiến lược xử lý lỗi nâng cao bạn cần biết

Để xây dựng hệ thống lớn, chiến lược xử lý lỗi bất đồng bộ JavaScript cần mở rộng ra ngoài những khối try...catch cơ bản, bao gồm quản lý nhiều Promise cùng lúc và phân loại lỗi rõ ràng. Các tính năng nâng cao này phần lớn dựa trên nền tảng của chuẩn ECMAScript mới. Nếu bạn muốn ôn lại, bài viết về ES6 JavaScript tính năng mới cần biết sẽ cung cấp góc nhìn toàn cảnh rất hữu ích.
So sánh Promise.all và Promise.allSettled: Khi nào nên “liều ăn nhiều”, khi nào cần “an toàn là trên hết”?
So sánh Promise.allSettled và Promise.all trong xử lý lỗi: Promise.all mang tính chất “fail-fast” (thất bại ngay khi có 1 lỗi), trong khi Promise.allSettled kiên nhẫn đợi tất cả hoàn thành và trả về trạng thái độc lập của từng Promise.
Đây là một khía cạnh cực kỳ quan trọng trong xử lý lỗi Promise JavaScript. Giả sử bạn đang load một trang Dashboard cần lấy dữ liệu từ 3 API khác nhau, việc chọn sai phương thức có thể làm sập toàn bộ trang.
| Phương thức | Đặc điểm hoạt động | Khi nào nên dùng? |
|---|---|---|
| Promise.all | Hủy toàn bộ tiến trình nếu 1 Promise bị reject (lỗi). | Khi các API phụ thuộc nhau chặt chẽ. Thiếu 1 cái là hỏng cả trang. |
| Promise.allSettled | Trả về mảng chứa trạng thái (fulfilled/rejected) của từng Promise. | Khi các API độc lập. Lỗi load quảng cáo không được làm sập tin tức. |
Dữ liệu cập nhật đầu năm 2026 cho thấy xu hướng thiết kế giao diện hiện đại đề cao tính “resilience” (khả năng phục hồi một phần). Do đó, mình thấy Promise.allSettled đang được ưu ái sử dụng nhiều hơn hẳn trong các error handling patterns thực tế.
Tạo Custom Error Class: Khi lỗi mặc định của JavaScript không đủ “ý nghĩa” để debug.
Việc tạo custom error class JavaScript giúp bạn phân loại các loại lỗi JavaScript một cách rành mạch như ValidationError hay DatabaseError, thay vì chỉ dùng Error object chung chung.
Trong một hệ thống phức tạp, các built-in error types (như TypeError, ReferenceError) là không đủ để bao quát logic nghiệp vụ. Khi bạn quăng ra một lỗi, bạn muốn biết ngay nó xuất phát từ đâu để gỡ lỗi nhanh chóng. Bằng cách sử dụng custom error classes, bạn có thể đính kèm thêm các metadata cực kỳ hữu ích.
// Kế thừa Error object mặc định
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = "DatabaseError";
this.query = query;
this.statusCode = 500;
}
}
// Bắt lỗi và kiểm tra loại lỗi chuyên biệt
try {
throw new DatabaseError("Không thể kết nối DB", "SELECT * FROM users");
} catch (error) {
if (error instanceof DatabaseError) {
console.log("Xử lý lỗi DB riêng biệt:", error.query);
}
}
Xử lý lỗi tập trung: Dùng Middleware trong Express.js và Error Boundary trong React
Triển khai centralized error handling giúp tách biệt hoàn toàn logic bắt lỗi khỏi business logic, giảm thiểu sự lặp lại code ở cả frontend và backend.
Trong backend error handling (như Node.js với Express.js), việc sử dụng một global error handling middleware ở cuối ứng dụng là tiêu chuẩn vàng. Nó bắt mọi lỗi lọt lưới và trả về một format chuẩn duy nhất cho client.
Còn đối với frontend error handling, đặc biệt là xử lý lỗi API trong React, chúng ta có khái niệm Error Boundary. Dù React cung cấp các cơ chế DOM JavaScript thao tác phần tử HTML rất mượt mà, nhưng nếu một component bị lỗi runtime, nó có thể làm trắng xóa toàn bộ màn hình. Error Boundary giúp khoanh vùng lỗi và hiển thị một UI thay thế. Ngoài ra, các thư viện quản lý state hiện đại như React Query (hay TanStack Query) cũng cung cấp sẵn các cơ chế xử lý lỗi và retry cực kỳ mạnh mẽ.
Những “vũ khí bí mật” giúp bạn làm chủ cuộc chơi xử lý lỗi

Ngoài việc bắt lỗi đơn thuần, việc chủ động ngăn chặn các request treo và ghi log hệ thống tự động là những kỹ năng phân biệt giữa một lập trình viên bình thường và một chuyên gia.
Đặt Timeout cho API Request bằng AbortController: Đừng để người dùng chờ đợi trong vô vọng
Cách đặt timeout cho request trong JavaScript chuẩn và hiện đại nhất là sử dụng AbortController hoặc hàm AbortSignal.timeout() để tự động hủy các request API vượt quá thời gian chờ.
Một trong những lỗi ngớ ngẩn nhất là để request treo vô thời hạn (hanging requests). Điều này ngốn băng thông và làm trải nghiệm người dùng tệ hại. Cơ chế timeout mechanism là bắt buộc phải có. Từ các phiên bản Node.js và trình duyệt mới nhất, JavaScript đã hỗ trợ AbortSignal.timeout() cực kỳ tiện lợi, giúp bạn tránh phải viết các logic setTimeout thủ công phức tạp.
async function fetchWithTimeout() {
try {
// Tự động hủy request sau 5 giây (5000ms)
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000)
});
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
console.error("Request đã bị hủy do quá thời gian chờ!");
// Nơi lý tưởng để kích hoạt retry mechanism
}
}
}
Ghi log lỗi hiệu quả: Từ console.log đến các dịch vụ chuyên nghiệp như Sentry, LogRocket.
Để gỡ lỗi và cách bắt lỗi runtime JavaScript trên môi trường production, bạn buộc phải ghi log lỗi JavaScript thông qua các dịch vụ chuyên dụng như Sentry thay vì phụ thuộc vào console.log.
Khi code chạy trên máy khách hàng, bạn không thể mở DevTools ra xem console.log được. Việc log errors lên các hệ thống giám sát giúp bạn nhận được thông báo ngay khi ứng dụng có vấn đề, kèm theo toàn bộ stack trace và thông tin thiết bị của người dùng. Tại Phạm Hải, bọn mình coi việc tích hợp Sentry hoặc LogRocket là bước bắt buộc trong quy trình QA trước khi release bất kỳ dự án nào.
“Rethrowing” – Khi nào nên bắt lỗi và khi nào nên “thả” cho nó đi?
Rethrowing (ném lỗi lại) là kỹ thuật error propagation, nơi bạn bắt lỗi ở một hàm nhỏ để ghi log hoặc xử lý nhẹ, sau đó dùng từ khóa throw đẩy lỗi đó lên cấp cao hơn xử lý tiếp.
Không phải lúc nào bắt được lỗi trong catch cũng là kết thúc. Đôi khi, hàm gọi API của bạn chỉ nên làm nhiệm vụ fetch data. Nếu có lỗi, nó log lại, rồi “rethrow” để component UI bên ngoài quyết định xem nên hiển thị thông báo “Mất mạng” hay “Sai mật khẩu”. Kỹ thuật này giúp giữ cho các hàm (functions) tuân thủ đúng nguyên tắc Đơn trách nhiệm (Single Responsibility).
Về cơ bản, việc nắm vững xử lý lỗi JavaScript try catch best practices không chỉ là viết thêm vài dòng code để đối phó với console đỏ lòm. Đó là tư duy về việc xây dựng một hệ thống vững chắc, có khả năng tự phục hồi và mang lại trải nghiệm mượt mà nhất cho người dùng. Đừng sợ lỗi, vì lỗi là một phần tất yếu của chu kỳ phát triển phần mềm. Hãy chủ động đón đầu nó bằng async/await, kiểm soát request bằng AbortController, và theo dõi sát sao qua các công cụ ghi log chuyên nghiệp.
Bạn có “best practice” hay câu chuyện “đau thương” nào về gỡ lỗi hệ thống muốn chia sẻ không? Để lại bình luận bên dưới nhé, mình rất muốn học hỏi từ cộng đồng!
Lưu ý: Các thông tin trong bài viết này chỉ mang tính chất tham khảo. Để có lời khuyên tốt nhất, vui lòng liên hệ trực tiếp với chúng tôi để được tư vấn cụ thể dựa trên nhu cầu thực tế của bạn.