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ả try và finally đề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.
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
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();
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.
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
}
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.
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;
}
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
}
}
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
}
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 là
"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.
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
}
}
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
});
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);
});
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.
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()));
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.
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
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
Comments
Bình luận