JavaScript Error Handling Try Catch Best Practices (Async/Await, API)

Xử Lý Lỗi JavaScript Try Catch Best Practices (Async/Await, API)

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/Awaittry...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 codefetch 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 trycatch, 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.allPromise.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.

This is an extremely important aspect of JavaScript Promise error handling. Suppose you are loading a Dashboard page that needs to get data from 3 different APIs, choosing the wrong method can crash the entire page.

Method Operating characteristics When should it be used?
Promise.all Cancel the entire process if a Promise is rejected (error). When APIs are tightly dependent on each other. If one is missing, the whole page is ruined.
Promise.allSettled Returns an array containing the status (fulfilled/rejected) of each Promise. When APIs are independent. Error loading ads should not cause news to crash.

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ế.

Create Custom Error Class: When JavaScript's default error is not "meaningful" enough to 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.

In a complex system, built-in error types (like TypeError, ReferenceError) are not enough to cover the business logic. When you throw an error, you want to immediately know where it came from so you can debug it quickly. By using custom error classes, you can attach extremely useful metadata.

// 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);
  }
}

Centralized error handling: Using Middleware in Express.js and Error Boundary in React

Implementing centralized error handling helps completely separate error handling logic from business logic, minimizing code repetition on both the frontend and 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.

As for frontend error handling, especially API error handling in React, we have the concept of Error Boundary. Although React provides DOM JavaScript mechanisms to manipulate HTML elements very smoothly, if a component has a runtime error, it can blank out the entire screen. Error Boundary helps localize errors and display an alternative UI. In addition, modern state management libraries such as React Query (or TanStack Query) also provide extremely powerful error handling and retry mechanisms.

These "secret weapons" help you master the error handling game

Beyond simply catching errors, proactively preventing suspended requests and automatic system logging are the skills that differentiate a normal programmer from an expert.

Đặ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" - When should you catch an error and when should you "let it go"?

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.

Do you have any "best practices" or "painful" stories about system debugging that you'd like to share? Leave a comment below, I really want to learn from the community!

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.

Categories: API & Backend JavaScript Lập Trình Web

mrhai

Để lại bình luận