← Lập trình JavaScript cơ bản

Bài 7 · Vận dụng · 20 phút

Xử lý lỗi & debug

Biên soạn bởi Nguyễn Anh Tuấn

Xử lý lỗi trong JavaScript: throw, try/catch/finally, đối tượng Error & stack trace, lỗi đồng bộ vs bất đồng bộ, debug bằng console & DevTools.

Viết code là sẽ gặp lỗi; kỹ năng nền là đọc hiểu thông điệp lỗi. Lỗi bị ném ra khi chương trình đang chạy gọi là exception (ngoại lệ). Ba loại gặp nhiều nhất: SyntaxError - viết sai cú pháp, JS không chạy nổi dòng nào của file; ReferenceError - gọi một cái tên chưa khai báo; và TypeError - giá trị có thật nhưng thao tác sai kiểu.

Khi chương trình sập, Node (hay trình duyệt) in một khối thông điệp. Đọc từ trên xuống: dòng đầu nói lỗi gì; các dòng at ...stack trace - vết của call stack, cho biết lỗi xảy ra trong hàm nào và hàm đó được ai gọi:

diem.js - output thật của node v22, đã lược các dòng at phía sau thành dấu ... (đường dẫn là chỗ file nằm trên máy của Mèo)

function layDiemToan(hocSinh) {
  return hocSinh.diem.toan;   // hocSinh.diem la undefined!
}
function inBangDiem() {
  const mun = { ten: "Mun" }; // quen khai bao truong diem
  console.log(layDiemToan(mun));
}
inBangDiem();

Kết quả khi chạy

/tmp/meo/diem.js:2
  return hocSinh.diem.toan;   // hocSinh.diem la undefined!
                      ^

TypeError: Cannot read properties of undefined (reading 'toan')
    at layDiemToan (/tmp/meo/diem.js:2:23)
    at inBangDiem (/tmp/meo/diem.js:6:15)
    ...
  • Dòng đầu: loại lỗi + thông điệp. “Cannot read properties of undefined (reading toan)” = vừa chấm .toan vào một thứ đang là undefined.
  • Các dòng at là stack trace: trên cùng là nơi lỗi xảy ra (file:dòng:cột), đi xuống là chuỗi hàm gọi.
  • SyntaxError chặn cả file từ đầu; ReferenceError và TypeError xảy ra lúc chạy, tại đúng dòng được in.

Lỗi trong JavaScript là một giá trị: object tạo bằng new Error("...") với .name, .message, .stack (stack trace, ghi lúc tạo). Vì Error là một class - như bài “Class & prototype” - bạn extends được nó để đặt tên cho lỗi của riêng mình:

loi-la-gia-tri.js - tạo Error chưa làm chương trình dừng

const loi = new Error("Khong doc duoc file ca.txt");
console.log(loi.message);      // Khong doc duoc file ca.txt
console.log(typeof loi.stack); // string - stack trace nam trong day
class LoiDuLieu extends Error {
  constructor(message) {
    super(message);
    this.name = "LoiDuLieu";   // ten rieng cho loai loi
  }
}
const loiHoSo = new LoiDuLieu("Ho so thieu truong 'ten'");
console.log(loiHoSo instanceof Error); // true - van la Error

Muốn tự báo lỗi, dùng throw: lỗi bị ném lan ngược call stack tới khi gặp người bắt; không ai bắt thì chương trình dừng và in stack trace - y như Bước 1:

napca.js - không ai bắt thì dừng (output đã lược khối đường-dẫn/dấu ^ ở đầu và các dòng at phía sau)

function napCa(soCa) {
  if (soCa < 0) {
    throw new Error("soCa phai >= 0, nhan duoc: " + soCa);
  }
  return "Da nap " + soCa + " con ca";
}
console.log(napCa(3));
console.log(napCa(-1)); // nem loi -> chuong trinh dung tai day

Kết quả khi chạy

Da nap 3 con ca
Error: soCa phai >= 0, nhan duoc: -1
    at napCa (/tmp/meo/napca.js:3:11)
    ...

Vì sao không throw một chuỗi?

JavaScript cho phép throw "hong roi" nhưng đừng: chuỗi không có .stack - khi sập, Node chỉ in đúng mấy chữ đó, không kèm vết gọi hàm. new Error("...") ghi stack trace lúc tạo: luôn ném Error hoặc lớp con.

Có những chỗ bạn biết trước là dễ hỏng - dữ liệu từ ngoài vào. Ví dụ kinh điển là JSON.parse bạn đã dùng ở bài “Mảng & object”: chuỗi JSON sai một dấu là ném SyntaxError. Bọc đoạn dễ hỏng trong try; lỗi rơi xuống catch; còn finally chạy mọi trường hợp - kể cả khi đã return:

cauhinh.js - JSON xấu không làm sập được chương trình: cả hai lần gọi đều chạy trọn finally

function docCauHinh(chuoi) {
  try {
    const cauHinh = JSON.parse(chuoi); // co the nem SyntaxError
    console.log("Doc OK, ten =", cauHinh.ten);
    return cauHinh;
  } catch (e) {
    console.log("JSON hong:", e.message);
    return null;                       // gia tri du phong an toan
  } finally {
    console.log("finally: luon chay"); // don dep o day
  }
}
docCauHinh('{"ten": "Mun"}');
docCauHinh("{ten: Mun}"); // du lieu xau

Kết quả khi chạy

Doc OK, ten = Mun
finally: luon chay
JSON hong: Expected property name or '}' in JSON at position 1 (line 1 column 2)
finally: luon chay

Đừng nuốt lỗi

Tệ nhất không phải là lỗi, mà là catch (e) {} rỗng: chương trình sai nhưng im lặng, bug quay lại sau và khó tìm gấp nhiều lần. Hãy bắt ở nơi bạn xử lý được (có giá trị dự phòng, có cách báo người dùng); tối thiểu phải log; không xử lý được thì để lỗi lan tiếp - hoặc throw e ném lại.

try/catch có một giới hạn quan trọng: nó chỉ bắt được lỗi đồng bộ - ném ra trong lúc khối try đang chạy. Còn callback của setTimeout chạy sau, khi khối try đã thoát từ lâu:

hen.js - lỗi vẫn làm sập chương trình dù có try/catch (output đã lược khối đường-dẫn/dấu ^ và dòng phiên bản Node; giữ nguyên các dòng at)

try {
  setTimeout(() => {
    throw new Error("Loi trong callback"); // nem SAU ~100ms
  }, 100);
} catch (e) {
  console.log("Bat duoc:", e.message);     // KHONG bao gio chay
}
console.log("Dong nay in truoc - try/catch da thoat tu lau");

Kết quả khi chạy

Dong nay in truoc - try/catch da thoat tu lau
Error: Loi trong callback
    at Timeout._onTimeout (/tmp/meo/hen.js:3:11)
    at listOnTimeout (node:internal/timers:585:17)
    at process.processTimers (node:internal/timers:521:7)

Để ý stack trace: bên gọi callback là Timeout._onTimeout - bộ hẹn giờ của Node, không phải khối try của bạn. Lỗi bất đồng bộ cần công cụ riêng: Promise có .catch, còn async/await đưa try/catch quay lại đúng nghĩa - nội dung chính của bài “Bất đồng bộ” ngay sau bài này. Hôm nay chỉ cần nhớ ranh giới: try/catch bắt lỗi đồng bộ.

Không phải bug nào cũng ném lỗi - nhiều khi chương trình chạy êm mà kết quả sai. Lúc đó cần debug: tìm xem sai từ đâu. Đồ nghề đầu tiên là console: ngoài log còn console.error (in ra kênh lỗi, DevTools tô đỏ), console.table (in mảng object thành bảng), và cặp console.time("nhan")console.timeEnd("nhan") bấm giờ đoạn code nằm giữa:

bang.js - console.table in mảng object thành bảng

const lopMeo = [
  { ten: "Mun", mon: "JS", diem: 8 },
  { ten: "Xam", mon: "HTML", diem: 9 }
];
console.table(lopMeo); // de soi du lieu hon log thuong

Kết quả khi chạy

┌─────────┬───────┬────────┬──────┐
│ (index) │ ten   │ mon    │ diem │
├─────────┼───────┼────────┼──────┤
│ 0       │ 'Mun' │ 'JS'   │ 8    │
│ 1       │ 'Xam' │ 'HTML' │ 9    │
└─────────┴───────┴────────┴──────┘

Log giúp thu hẹp phạm vi: in giá trị ở điểm giữa đoạn nghi ngờ, nửa nào sai thì khoanh tiếp nửa đó - vài lần là tới đúng dòng gây bug. Khi log không đủ, mở DevTools của trình duyệt (phím F12, tab Sources) đặt breakpoint vào một dòng, hoặc viết thẳng debugger; vào code: chương trình dừng tại đó, bạn xem được giá trị mọi biến rồi chạy tiếp từng bước. Lưu ý: debugger chỉ có tác dụng khi DevTools đang mở - node chạy bình thường sẽ bỏ qua nó.

Cuối cùng là phòng thủ: fail fast - kiểm tra input ngay đầu hàm, thấy sai thì throw ngay. Hàm napCa ở Bước 2 làm đúng kiểu này: lỗi hiện sớm, gần nguyên nhân, thay vì để giá trị sai chui qua nhiều hàm rồi gây TypeError ở một nơi khó lần lại.

Tiếp theo

Bạn đã nắm xử lý lỗi đồng bộ và đồ nghề debug. Bài sau - “Bất đồng bộ” - là nơi lỗi không còn đứng yên một chỗ: callback, Promise với .catch, async/await - nơi try/catch của bài này trở lại đúng vai.

Câu hỏi thường gặp

Trong JavaScript, hai từ gần như chỉ một thứ. Chính xác hơn: exception là giá trị bị throw ra giữa chừng, còn Error là loại object thường được ném. JS cho phép ném mọi giá trị, nhưng quy ước nghề là luôn ném Error (hoặc lớp con của nó) để có stack trace.

Trên các engine hiện đại (V8 của Chrome/Node…), chi phí thêm của một khối try/catch nhỏ tới mức không cần bận tâm. Thứ nên tránh là dùng SAI VIỆC: lấy try/catch thay if để điều khiển luồng chạy bình thường, hoặc bọc cả chương trình trong một khối try khổng lồ rồi không biết lỗi từ đâu ra.

Tình huống DỰ KIẾN được (tìm không thấy, ô nhập bỏ trống) → trả null/giá trị mặc định rồi xử lý bằng if. Tình huống KHÔNG được phép xảy ra (gọi hàm sai kiểu, dữ liệu hỏng) → throw để dừng sớm. C không có exception nên mọi lỗi đều báo qua giá trị trả về - so sánh hai triết lý này rất đáng đọc.

Xử lý lỗi kiểu C - khoá C nâng cao →

Không chắc. Vì throw ném được mọi giá trị nên e có thể là chuỗi, số… Thư viện tử tế luôn ném Error, nhưng khi viết code phòng thủ, kiểm tra e instanceof Error trước khi đọc e.message/e.stack là thói quen tốt.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Đoạn try { setTimeout(() => { throw new Error("X"); }, 0); } catch (e) { console.log("bat duoc"); } chạy ra sao?

  1. 1

    Đọc thông điệp lỗi

    Chạy riêng từng dòng sau (mỗi dòng một file) và ghi lại LOẠI lỗi: meo( · console.log(banMeo) · null.ten.

    Hoàn thành khi: SyntaxError (Unexpected end of input) · ReferenceError (banMeo is not defined) · TypeError (Cannot read properties of null).

  2. 2

    Lần theo stack trace

    Viết a() gọi b(), b() gọi c(), còn c() ném new Error("o day"). Chạy rồi đọc các dòng at từ trên xuống.

    Hoàn thành khi: Thứ tự: at cat bat a - từ nơi ném lỗi ngược ra nơi gọi đầu tiên.

  3. 3

    Lỗi tự đặt tên

    Viết class LoiDangNhap extends Error, ném nó khi mật khẩu là chuỗi rỗng; bắt và in e.name + ": " + e.message.

    Hoàn thành khi: In ra LoiDangNhap: ...; thử e instanceof Error phải ra true.

  4. 4

    JSON an toàn

    Viết hàm docJSON(chuoi): parse được thì trả object, hỏng thì log message rồi trả null - không bao giờ làm sập chương trình.

    Hoàn thành khi: Chuỗi tốt → object; chuỗi "xyz" → log lỗi + nhận null; dòng lệnh phía sau vẫn chạy.

  5. 5

    try đứng nhầm chỗ

    Chạy lại demo Bước 4, rồi chuyển try/catch vào BÊN TRONG callback của setTimeout và chạy lại.

    Hoàn thành khi: Bọc ngoài: vẫn sập sau ~100ms. Bọc trong callback: in Bat duoc: ..., chương trình sống.

  6. 6

    Đo bằng console

    Tạo mảng 3 bạn mèo (ten, diem) in bằng console.table; bấm giờ vòng lặp cộng dồn tới 1e7 bằng console.time("vong lap")console.timeEnd("vong lap").

    Hoàn thành khi: Bảng có cột ten/diem; thời gian in dạng vong lap: 15.349ms - con số mỗi máy mỗi khác.