Học bằng cách đọc theo thứ tự

Bắt ngoại lệ — try / catch / finally

Bắt ngoại lệ lúc chạy bằng try và catch, dọn dẹp trong finally, ném lại bằng throw và rẽ nhánh theo loại lỗi.

Khi bạn dùng JSON.parse để đưa một chuỗi nhận được trở lại thành object, chỉ cần chuỗi sai một ký tự là chương trình dừng ngay tại dòng đó, và phần hiển thị hay lưu dữ liệu viết bên dưới đều không chạy. Bạn cũng không thể dùng if để chặn trước mọi nguyên nhân khiến nó dừng.

Bài này nói về try / catch để bắt lấy ngoại lệ đã làm code dừng lại, và finally là khối luôn được chạy.

Bắt lấy ngoại lệ — try và catch

Giả sử bạn muốn parse chuỗi cài đặt nhận từ server rồi áp dụng lên trang. Nếu chuỗi bị sai định dạng, JSON.parse sẽ ném ra một ngoại lệ (exception — lỗi làm gián đoạn quá trình xử lý) ngay tại dòng đó, và không dòng nào bên dưới được chạy. Trang vẫn trống trơn, và mọi thứ dừng ở đó.

Khi ngoại lệ xảy ra bên trong try (khối bao quanh đoạn code có thể thất bại), chương trình chuyển sang catch (khối nhận ngoại lệ). error trong catch (error) chỉ là tên bạn đặt cho giá trị nhận được (đổi thành err hay tên khác cũng chạy bình thường). Nó chứa name là tên loại lỗi, và message là phần mô tả lỗi.

// Chuỗi còn sót dấu phẩy thừa ở cuối
const configText = '{"theme":"dark","fontSize":14,}';

try {
  const config = JSON.parse(configText);
  console.log(config.theme);          // dòng này không được chạy
} catch (error) {
  // khi có ngoại lệ, chương trình nhảy vào đây
  console.log("Không tải được cài đặt");  // Không tải được cài đặt
  console.log(error.name);                // SyntaxError
}

// nhờ có catch nên các dòng tiếp theo vẫn chạy
console.log("Hiển thị bằng cài đặt mặc định");   // Hiển thị bằng cài đặt mặc định
Không dùng try và có dùng try
parse màkhông có tryNgoại lệ xảy ratại dòng đóKhông có chỗnào để chuyểnCác dòng saukhông chạyparse bêntrong tryNgoại lệ xảy ratại dòng đóChuyển sangcatch (error)Các dòng sautry được chạy
Dù viết cách nào, dòng ngay dưới dòng ném ngoại lệ cũng không chạy. Khi có try, chương trình chuyển sang catch rồi chạy tiếp tới các dòng sau try.

Một khi đã chuyển sang catch, chương trình không quay lại chạy tiếp dòng đã ném ngoại lệ, nên hãy viết phần hiển thị thay thế hoặc giá trị mặc định bên trong catch. Nội dung của message khác nhau tùy trình duyệt, nên khi cần phân biệt loại lỗi, hãy đọc name.

Lấy tiêu đề từ một bản nháp đã lưu. savedDraftbrokenDraft đã được khai báo sẵn.

① Trong restoreTitle, hãy parse chuỗi và trả về tiêu đề.

② Nếu thất bại, hãy hiển thị tên loại lỗi và trả về "Bản nháp chưa đặt tên".

③ Hãy gọi hàm với cả 2 chuỗi và hiển thị kết quả theo dạng "Tiêu đề: Báo cáo tháng 9".

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Chạy dù thành công hay thất bại — finally

Những việc dọn dẹp như ẩn chỉ báo đang tải hay đóng kết nối đã mở thì cần làm dù xử lý thành công hay thất bại. Nếu viết cùng một dòng ở cả try lẫn catch, sau này bạn dễ sửa chỗ này mà quên chỗ kia, và hai nhánh sẽ chạy lệch nhau.

finally (khối luôn được chạy khi rời khỏi try) được viết sau catch. Dù try chạy hết đến cuối hay catch đã nhận ngoại lệ, nó luôn được chạy ngay trước khi rời đi. Bạn cũng có thể bỏ catch và chỉ viết try với finally.

function loadReport(text) {
  try {
    const report = JSON.parse(text);
    console.log(report.title);
  } catch (error) {
    console.log("Tải thất bại");
  } finally {
    // luôn chạy khi rời khỏi try
    console.log("Đã ẩn chỉ báo đang tải");
  }
}

// Khi thành công
loadReport('{"title":"Doanh thu tháng 9"}');  // Doanh thu tháng 9 → Đã ẩn chỉ báo đang tải

// Khi thất bại
loadReport('{"title":}');                     // Tải thất bại → Đã ẩn chỉ báo đang tải
Hai đường đi cùng kết thúc ở một chỗ
Truyền chuỗihợp lệtry chạyđến cuốicatch bịbỏ quafinallyđược chạyTruyền chuỗisai định dạngNgoại lệ xảy ragiữa chừng trycatchnhận lấyfinallyđược chạy
Hàng trên và hàng dưới đi qua những chỗ khác nhau, nhưng điểm cuối bên phải là một. finally được chạy dù code thành công hay thất bại.

finally chạy sau khi phần code trong try hoặc catch đã xong. Vì vậy trong đoạn code phía trên, thông báo dọn dẹp xuất hiện sau tiêu đề khi thành công, và sau thông báo lỗi khi thất bại.

Không đọc được biến khai báo trong try từ bên ngoài

Biến khai báo bằng const hoặc let bên trong try chỉ dùng được trong chính khối đó. Nếu đọc nó từ catch, finally hay các dòng phía sau thì sẽ gặp ReferenceError. Với những giá trị cần dùng về sau, hãy khai báo bằng let trước try, rồi chỉ gán giá trị bên trong try.

Nhập dữ liệu thành viên từng dòng một và hiển thị tiến độ. rowsprocessed đã được khai báo sẵn, còn dòng thứ 2 bị sai định dạng.

① Hãy parse từng dòng và hiển thị theo dạng "Minh: gói năm".

② Nếu parse thất bại, hãy hiển thị "Có một dòng không nhập được".

③ Dù thành công hay thất bại, hãy tăng processed thêm 1 và hiển thị theo dạng "Đã xử lý 1 / 3".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Báo cho phía gọi — throw bên trong catch

Giả sử một hàm đọc dữ liệu đặt chỗ bắt ngoại lệ bằng catch rồi chỉ ghi log. Khi đó hàm vẫn kết thúc bình thường, phía gọi không hề biết đã có lỗi, tiếp tục dùng giá trị trả về, rồi lại dừng ở một dòng khác cách xa nguyên nhân thật.

Nếu truyền nguyên errorcatch nhận được cho throw (câu lệnh ném ra ngoại lệ), cùng ngoại lệ đó sẽ được chuyển tiếp lên phía gọi. Hàm vẫn tự ghi log được, còn việc xử lý thế nào thì để phía gọi quyết định.

function readEntry(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    console.log(`Không đọc được dữ liệu đặt chỗ: ${error.name}`);
    throw error;   // ném lại nguyên ngoại lệ đã nhận
  }
}

try {
  const entry = readEntry('{"name":"Linh","seats":}');
  console.log(entry.name);              // dòng này không được chạy
} catch (error) {
  console.log("Đã hủy đặt chỗ");        // Đã hủy đặt chỗ
}
Ném lại và không ném lại
catch cóthrow errorSyntaxError đượcchuyển ra ngoàicatch bên ngoàinhận lấy"Đã hủyđặt chỗ"catch không cóthrow errorGiá trị trả vềlà undefinedTypeError tạientry.namePhía gọi nhậnmột lỗi khác
Ở hàng dưới, ngoại lệ bị nuốt mất bên trong hàm, rồi một lỗi khác lại xảy ra ở dòng đọc thuộc tính của undefined. Khi ném lại, SyntaxError gốc sẽ tới được phía gọi.

Nếu phía gọi cũng cần xử lý ngoại lệ đã bị bắt bên trong hàm, hãy ném lại nó bằng cách viết throw error; ở cuối catch. Các dòng sau throw không được chạy, nên dòng ghi log phải đặt trước nó.

catch rỗng khiến lỗi như chưa từng xảy ra

Để trống catch (error) { } thì ngoại lệ trông như đã biến mất, nhưng việc thất bại không được ghi lại ở đâu cả, nên sau này bạn không lần ra được nguyên nhân. Kết quả hay gặp nhất là một trang cứ trống trơn. Hãy luôn làm ít nhất một trong các việc sau: hiển thị thông báo, dùng giá trị mặc định, hoặc ném lại.

Tải bố cục đã lưu. savedLayout, layouterrorLog đã được khai báo sẵn.

① Trong loadLayout, hãy parse chuỗi và trả về kết quả.

② Nếu thất bại, hãy thêm tên loại lỗi vào errorLog rồi ném lại.

③ Hãy gán kết quả cho layout, và nếu thất bại thì hiển thị "Hiển thị bố cục mặc định".

④ Hãy hiển thị số cột và log theo dạng "Số cột: 2" và "Log: SyntaxError".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Rẽ nhánh theo loại lỗi — instanceof và name

catch nhận mọi ngoại lệ, bất kể loại gì. Nhưng thông báo bạn muốn hiển thị cho người dùng sẽ khác nhau tùy theo dữ liệu bị sai định dạng hay giá trị cần đọc không tồn tại.

Với instanceof (toán tử kiểm tra giá trị bên trái có được tạo từ loại bên phải hay không), bạn kiểm tra được loại lỗi, chẳng hạn error instanceof SyntaxError. error.name cũng chứa tên của loại đó dưới dạng chuỗi.

const applicant = { name: "Minh", contact: null };

try {
  console.log(applicant.contact.email);
} catch (error) {
  if (error instanceof SyntaxError) {
    console.log("Dữ liệu sai định dạng");                  // không đi vào đây
  } else if (error instanceof TypeError) {
    console.log("Không có giá trị cần đọc");     // Không có giá trị cần đọc
  } else {
    console.log(`Loại lỗi ngoài dự kiến: ${error.name}`);
  }

  console.log(error.name);                       // TypeError
}
Ba ngoại lệ, cùng một catch
Ngoại lệ xảy ratrong tryparse JSONsai định dạngĐọc contact khinó đang là nullGọi toFixedvới 200 chữ sốname làSyntaxErrorname làTypeErrorname làRangeErrorBáo dữ liệusai định dạngBáo không cógiá trị cần đọcGhi log là lỗingoài dự kiến
Cả ba đều tới cùng một chỗ catch (error), nhưng mỗi cái có name khác nhau. Nhờ kiểm tra loại lỗi, bạn hiển thị được thông báo riêng cho từng loại.

Những ngoại lệ không khớp với điều kiện nào sẽ được gom vào nhánh else cuối cùng. Bảng dưới đây liệt kê 4 loại lỗi bạn hay gặp và trường hợp mỗi loại xảy ra.

LoạiXảy ra khiVí dụ code gây ra lỗi
SyntaxErrorChuỗi không đúng định dạng JSONJSON.parse("{,}")
TypeErrorĐọc thuộc tính của null hoặc undefinedapplicant.contact.email
RangeErrorTruyền giá trị vượt quá phạm vi cho phép, chẳng hạn số chữ số(1980).toFixed(200) (số chữ số tối đa là 100)
ReferenceErrorĐọc một tên chưa từng được khai báoconsole.log(total)

Xử lý các chuỗi dữ liệu gửi về từ cảm biến nhiệt độ. payloads đã được khai báo sẵn, và 2 trong 3 chuỗi có vấn đề.

① Hãy parse từng chuỗi và hiển thị theo dạng "T-1: 21.5 độ". Nhiệt độ được làm tròn đến 1 chữ số thập phân.

② Nếu sai định dạng, hãy hiển thị "Dữ liệu nhận được sai định dạng".

③ Nếu không có nhiệt độ, hãy hiển thị "Không có dữ liệu nhiệt độ".

④ Nếu không thuộc hai trường hợp trên, hãy hiển thị "Lỗi ngoài dự kiến: " kèm tên loại lỗi.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1try { console.log(1); } finally { console.log(2); } sẽ hiển thị gì?

Câu 2Nếu catch trong một hàm chỉ ghi log mà không có throw error;, phía gọi sẽ nhận được giá trị trả về nào?

Câu 3Toán tử nào dùng để kiểm tra loại của errorcatch nhận được?