This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Một ứng dụng JavaScript chuyên nghiệp không phải là ứng dụng không bao giờ lỗi, mà là ứng dụng biết dự đoán, bắt giữ và phục hồi trước lỗi một cách duyên dáng. Trong bài 6 này, chúng ta sẽ đi từ cú pháp nền tảng try/catch/finally, hiểu sâu về đối tượng Error và các kiểu lỗi tích hợp, tự thiết kế các lớp lỗi tùy chỉnh, xử lý lỗi bất đồng bộ với async/await và Promise, áp dụng các mẫu phục hồi (retry, fallback, Result pattern), cho đến việc làm chủ bộ công cụ gỡ rối (Debugging) hiện đại của trình duyệt.

1. try / catch / finally

Khối try...catch là cơ chế xử lý lỗi đồng bộ cốt lõi của JavaScript. Code có nguy cơ ném lỗi được đặt trong khối try. Nếu một lỗi (exception) được ném ra ở bất kỳ dòng nào trong try, luồng thực thi sẽ lập tức nhảy sang khối catch, bỏ qua mọi dòng còn lại trong try. Tham số của catch (thường đặt tên là e hoặc err) chính là đối tượng lỗi mô tả nguyên nhân.

Khối finally là phần đặc biệt: nó luôn luôn được thực thi, bất kể khối try chạy thành công, ném lỗi, hay thậm chí có lệnh return bên trong try/catch. Vì lý do này, finally là nơi lý tưởng để dọn dẹp tài nguyên (đóng kết nối, ẩn spinner loading, giải phóng khóa) — những việc bắt buộc phải xảy ra dù kết quả ra sao.

Từ ES2019, JavaScript hỗ trợ optional catch binding: nếu bạn không cần dùng đến đối tượng lỗi, có thể viết catch {} mà không cần khai báo tham số. Điều này giúp code gọn gàng hơn khi bạn chỉ quan tâm rằng "có lỗi đã xảy ra" mà không quan tâm chi tiết lỗi.

Một điểm tinh tế về luồng thực thi: nếu cả tryfinally đều có return, thì giá trị return trong finally sẽ ghi đè giá trị của try. Đây là một cái bẫy thường gặp — nên tránh đặt return trong finally để code dễ đoán. Hãy nhớ rằng try/catch chỉ bắt được lỗi đồng bộ; lỗi trong callback bất đồng bộ (như setTimeout) sẽ không bị bắt bởi try/catch bao ngoài.

Cú pháp cơ bản try/catch/finally
function parseConfig(jsonText) {
  try {
    const data = JSON.parse(jsonText); // có thể ném SyntaxError
    console.log("Phân tích thành công:", data);
    return data;
  } catch (e) {
    console.error("Lỗi khi phân tích JSON:", e.message);
    return null; // giá trị fallback an toàn
  } finally {
    console.log("Khối finally luôn chạy — dọn dẹp tại đây.");
  }
}

parseConfig('{"port": 8080}');   // hợp lệ
parseConfig('{port: 8080}');     // sai cú pháp -> SyntaxError
Optional catch binding & finally cleanup
function isValidJSON(text) {
  try {
    JSON.parse(text);
    return true;
  } catch {            // ES2019: không cần khai báo tham số lỗi
    return false;
  }
}

let isLoading = true;
function loadData() {
  try {
    // ... giả lập tải dữ liệu ...
    throw new Error("Mạng ngắt kết nối");
  } catch (e) {
    console.warn("Bắt được:", e.message);
  } finally {
    isLoading = false; // luôn tắt spinner dù thành công hay thất bại
    console.log("isLoading =", isLoading);
  }
}
loadData();
▶ Thử chạy: Thứ tự thực thi try/catch/finally

2. Error Object & Các Built-in Error Types

Khi một lỗi được ném ra, JavaScript tạo một đối tượng Error (hoặc lớp con của nó). Mọi đối tượng lỗi đều có ba thuộc tính cốt lõi: name (tên loại lỗi, ví dụ "TypeError"), message (thông điệp mô tả do bạn hoặc engine cung cấp), và stack (chuỗi stack trace cho biết lỗi xảy ra ở đâu trong code — cực kỳ quý giá khi gỡ rối). Thuộc tính stack không thuộc chuẩn ECMAScript chính thức nhưng được mọi engine hiện đại hỗ trợ.

JavaScript cung cấp một loạt kiểu lỗi tích hợp, mỗi kiểu phản ánh một loại sự cố khác nhau. Hiểu rõ ý nghĩa của từng kiểu giúp bạn chẩn đoán nhanh hơn:

  • TypeError: Thao tác trên giá trị không đúng kiểu — ví dụ gọi một thứ không phải hàm, hoặc đọc thuộc tính của undefined/null. Đây là lỗi runtime phổ biến nhất.
  • RangeError: Một giá trị nằm ngoài phạm vi cho phép — ví dụ new Array(-1) hoặc đệ quy vô hạn gây tràn stack.
  • ReferenceError: Truy cập một biến chưa được khai báo trong scope hiện tại.
  • SyntaxError: Code sai cú pháp — thường được ném khi parse code hoặc khi JSON.parse() nhận chuỗi không hợp lệ.
  • Error: Lớp cơ sở tổng quát, là cha của tất cả các kiểu trên.

Một mẹo hữu ích là dùng instanceof để phân nhánh xử lý theo từng kiểu lỗi, vì TypeError cũng đồng thời là một Error.

Đọc thuộc tính của Error
try {
  null.foo; // đọc thuộc tính của null
} catch (e) {
  console.log("name   :", e.name);    // "TypeError"
  console.log("message:", e.message); // mô tả từ engine
  console.log("stack  :", e.stack);   // dấu vết gọi hàm
  console.log("Là Error?", e instanceof Error); // true
}
Phân nhánh theo kiểu lỗi
function classify(fn) {
  try {
    fn();
  } catch (e) {
    if (e instanceof TypeError)      console.log("Lỗi kiểu dữ liệu");
    else if (e instanceof RangeError) console.log("Lỗi phạm vi");
    else if (e instanceof ReferenceError) console.log("Biến chưa khai báo");
    else console.log("Lỗi khác:", e.name);
  }
}

classify(() => undefined.x);     // TypeError
classify(() => new Array(-5));   // RangeError
classify(() => unknownVar);      // ReferenceError

Bảng tổng hợp nhanh các kiểu lỗi tích hợp và tình huống điển hình:

Kiểu lỗi Khi nào xảy ra Ví dụ kích hoạt
TypeError Sai kiểu / thao tác không hợp lệ null.foo, (5)()
RangeError Giá trị ngoài phạm vi new Array(-1)
ReferenceError Biến chưa khai báo x + 1 (x không tồn tại)
SyntaxError Cú pháp sai khi parse JSON.parse("{")
URIError Sai khi mã hóa/giải mã URI decodeURIComponent("%")

3. throw & Custom Error Classes

Lệnh throw cho phép bạn chủ động ném ra một ngoại lệ. Về mặt kỹ thuật, JavaScript cho phép throw bất kỳ giá trị nào — chuỗi, số, object thuần. Tuy nhiên, đây là một thực hành xấu: chỉ nên ném đối tượng kế thừa từ Error, vì chỉ chúng mới có stack, name và tương thích với các công cụ gỡ lỗi. Ném một chuỗi như throw "lỗi" sẽ khiến bạn mất hoàn toàn stack trace.

Để biểu diễn các lỗi nghiệp vụ riêng (ví dụ lỗi xác thực, lỗi quyền truy cập), ta tạo Custom Error Classes bằng cách extends Error. Trong constructor, hãy gọi super(message) để thiết lập message, rồi gán this.name bằng tên lớp. Việc này giúp bạn dùng instanceof để bắt chính xác từng loại lỗi nghiệp vụ và đính kèm dữ liệu ngữ cảnh (như mã trạng thái HTTP, tên trường bị lỗi).

Từ ES2022, đối tượng Error hỗ trợ tùy chọn cause — cho phép bạn "bọc" một lỗi gốc bên trong một lỗi cấp cao hơn mà không mất thông tin ban đầu: new Error("Tải user thất bại", { cause: originalError }). Đây là kỹ thuật quan trọng để xây dựng chuỗi lỗi (error chaining), giúp truy vết nguyên nhân gốc rễ xuyên qua nhiều tầng của ứng dụng.

Định nghĩa Custom Error Class
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field; // dữ liệu ngữ cảnh tùy chỉnh
  }
}

function checkAge(age) {
  if (typeof age !== "number") {
    throw new ValidationError("Tuổi phải là số", "age");
  }
  if (age < 0) {
    throw new ValidationError("Tuổi không thể âm", "age");
  }
  return true;
}
Bắt lỗi với instanceof
try {
  checkAge(-5);
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`Trường '${e.field}' lỗi: ${e.message}`);
  } else {
    throw e; // không phải lỗi ta xử lý -> ném lại
  }
}
Error cause (ES2022) — bọc lỗi gốc
function loadUser(id) {
  try {
    JSON.parse("{ broken json");
  } catch (original) {
    throw new Error(`Không thể tải user #${id}`, { cause: original });
  }
}

try {
  loadUser(42);
} catch (e) {
  console.log("Lỗi cấp cao:", e.message);
  console.log("Nguyên nhân gốc:", e.cause.name); // SyntaxError
}
▶ Thử chạy: Custom Error + instanceof

4. Async Error Handling

Lỗi bất đồng bộ là nơi nhiều lập trình viên vấp ngã, vì try/catch thông thường không bắt được lỗi xảy ra trong tương lai. May mắn thay, với async/await, bạn có thể bọc lời gọi await trong try/catch như thể nó là code đồng bộ. Khi một Promise bị reject, await sẽ "ném" lỗi đó ra và catch bắt được — đây là cách viết xử lý lỗi async sạch và dễ đọc nhất.

Với Promise thuần (không dùng await), bạn gắn .catch() vào cuối chuỗi để bắt mọi lỗi xảy ra ở bất kỳ bước .then() nào phía trên. Một .catch() duy nhất ở cuối có thể xử lý lỗi cho toàn bộ chuỗi.

Khi nhiều tác vụ bất đồng bộ chạy song song, Promise.all() sẽ fail-fast — chỉ cần một Promise reject là toàn bộ thất bại. Để chạy tất cả và thu thập kết quả của từng cái (cả thành công lẫn thất bại) mà không bị ngắt giữa chừng, hãy dùng Promise.allSettled(). Nó trả về mảng các object có status"fulfilled" (kèm value) hoặc "rejected" (kèm reason).

Cuối cùng, mọi Promise bị reject mà không có .catch() sẽ kích hoạt sự kiện toàn cục unhandledrejection trên window. Lắng nghe sự kiện này là lưới an toàn cuối cùng để ghi log những lỗi async bị bỏ sót trong môi trường production.

try/catch với async/await
async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    console.error("Tải user thất bại:", e.message);
    return null; // fallback
  }
}
.catch() trên chuỗi Promise & unhandledrejection
fetch("/api/data")
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error("Lỗi chuỗi Promise:", err.message));

// Lưới an toàn cuối cùng cho lỗi async bị bỏ sót
window.addEventListener("unhandledrejection", (event) => {
  console.error("Promise reject chưa xử lý:", event.reason);
  event.preventDefault(); // ngăn log mặc định của trình duyệt
});
Promise.allSettled — không fail-fast
const tasks = [
  Promise.resolve("OK 1"),
  Promise.reject(new Error("Hỏng 2")),
  Promise.resolve("OK 3")
];

const results = await Promise.allSettled(tasks);
results.forEach((r, i) => {
  if (r.status === "fulfilled") console.log(i, "✓", r.value);
  else console.log(i, "✗", r.reason.message);
});
▶ Thử chạy: Bắt lỗi async + allSettled

5. Error Boundaries & Graceful Degradation

Bắt lỗi mới chỉ là một nửa câu chuyện; nửa còn lại là phục hồi một cách duyên dáng (graceful degradation) để ứng dụng vẫn dùng được dù một phần bị hỏng. Có vài mẫu thiết kế cốt lõi giúp đạt điều này.

Fallback values: Khi một thao tác thất bại, trả về một giá trị mặc định an toàn thay vì để lỗi lan truyền — ví dụ một mảng rỗng, một cấu hình mặc định, hay nội dung cache cũ. Người dùng thấy một trải nghiệm suy giảm nhưng không phải màn hình trắng.

Retry với backoff: Nhiều lỗi mạng chỉ là tạm thời. Thay vì bỏ cuộc ngay, ta thử lại vài lần, mỗi lần chờ lâu hơn (exponential backoff: 100ms, 200ms, 400ms...) để tránh dồn tải lên server đang gặp sự cố.

Result / Either pattern: Thay vì ném lỗi, hàm trả về một object mô tả rõ ràng thành công hay thất bại, kiểu { ok: true, value } hoặc { ok: false, error }. Cách này buộc người gọi phải xử lý cả hai nhánh một cách tường minh, lấy cảm hứng từ ngôn ngữ như Rust và Go — rất phù hợp cho logic nghiệp vụ nơi "thất bại" là kết quả hợp lệ chứ không phải sự cố bất thường.

Retry với exponential backoff
async function retry(fn, attempts = 3, delay = 100) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === attempts - 1) throw e; // hết lượt -> ném lỗi cuối
      const wait = delay * 2 ** i;     // 100, 200, 400...
      console.warn(`Lần ${i + 1} hỏng, thử lại sau ${wait}ms`);
      await new Promise(r => setTimeout(r, wait));
    }
  }
}

// retry(() => fetch("/api/flaky").then(r => r.json()));
Result / Either pattern + fallback
function safeParse(text) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

const r = safeParse("{ hỏng");
if (r.ok) {
  console.log("Dữ liệu:", r.value);
} else {
  console.warn("Parse lỗi, dùng mặc định:", r.error);
  const config = { theme: "light" }; // fallback value
  console.log("Config mặc định:", config);
}

6. Debugging Toolkit

Gỡ rối hiệu quả không chỉ là rải console.log khắp nơi. Đối tượng console có cả một bộ công cụ mạnh mẽ: console.table() hiển thị mảng/object dưới dạng bảng dễ đọc; console.group()/console.groupEnd() nhóm các log liên quan thành cây có thể thu gọn; console.trace() in ra toàn bộ stack trace tại điểm gọi; console.assert(điều_kiện, msg) chỉ log khi điều kiện sai; còn console.time()/console.timeEnd() đo thời gian thực thi một đoạn code.

Mạnh hơn console.log là lệnh debugger. Khi DevTools đang mở, gặp dòng debugger; trình duyệt sẽ tạm dừng thực thi ngay tại đó, cho phép bạn xem xét giá trị mọi biến trong scope, bước qua từng dòng (step over/into/out), và đánh giá biểu thức trong console. Bạn cũng có thể đặt breakpoint trực tiếp trong tab Sources của Chrome DevTools mà không cần sửa code, kể cả conditional breakpoint chỉ dừng khi điều kiện đúng.

Khi code đã được build/minify, source maps ánh xạ ngược code đã biên dịch về mã nguồn gốc, giúp stack trace và breakpoint hiển thị đúng dòng trong file gốc thay vì file rối loạn. Cuối cùng, hãy học cách đọc stack trace: dòng trên cùng là nơi lỗi xảy ra, các dòng dưới là chuỗi hàm đã gọi đến nó — đọc từ trên xuống để truy ngược con đường dẫn tới lỗi.

console nâng cao: table / group / assert / time
const users = [
  { id: 1, name: "An", role: "admin" },
  { id: 2, name: "Bình", role: "user" }
];
console.table(users); // hiển thị dạng bảng

console.group("Khởi tạo");
console.log("Bước 1: tải config");
console.log("Bước 2: kết nối DB");
console.groupEnd();

console.assert(users.length === 2, "Số user phải là 2");

console.time("vòng lặp");
let sum = 0;
for (let i = 0; i < 1e6; i++) sum += i;
console.timeEnd("vòng lặp"); // in thời gian chạy
debugger statement & console.trace
function calculateTotal(items) {
  let total = 0;
  for (const item of items) {
    debugger; // DevTools mở -> dừng tại đây mỗi vòng lặp
    total += item.price;
  }
  return total;
}

function deep() {
  console.trace("Đường dẫn gọi đến đây:");
}
function middle() { deep(); }
middle(); // in toàn bộ stack: deep <- middle <- (top)

7. Câu hỏi trắc nghiệm ôn tập

Quiz 1 — finally

Khối finally được thực thi trong trường hợp nào?

Quiz 2 — Kiểu lỗi

Đọc thuộc tính của null, ví dụ null.foo, sẽ ném ra lỗi loại nào?

Quiz 3 — Async

Bạn muốn chạy nhiều Promise song song và thu thập kết quả của TẤT CẢ kể cả khi một số reject. Dùng gì?

Tải file code thực hành minh họa bài học

File script tổng hợp đầy đủ try/catch/finally, các Custom Error Class với cause, xử lý lỗi async/await, mẫu retry với backoff, Result pattern và bộ công cụ debugging:

Tải về js_errors.js

Related Articles

Bài viết liên quan trong series

Lesson 7: Mastering Regular Expressions in JavaScript Bài 7: Làm Chủ Biểu Thức Chính Quy (Regex) trong JavaScript Lesson 5: Functional JS: Immutability & High-Order Functions Bài 5: JavaScript Functional Programming — Làm chủ High-Order Functions Back to JavaScript Series Overview Quay lại Lộ trình JavaScript Series