try/catch là lưới an toàn, không phải dây an toàn
Khi một dòng JavaScript ném ra lỗi, chương trình dừng ngay lập tức và lỗi lan ngược lên call stack. Nếu không có gì bắt nó, chương trình sẽ crash (trong Node) hoặc in ra một bức tường chữ đỏ trong console (trên trình duyệt). try/catch là cách để bạn chặn lỗi đó lại, như muốn nói "tôi biết chỗ này có thể hỏng, và đây là việc tôi muốn làm thay vào đó."
Hình dạng cơ bản:
JSON.parse ném ra một SyntaxError. Chương trình nhảy ngay tới khối catch với lỗi được gán cho err. Lệnh console.log thứ ba vẫn chạy: sự cố đã được khoanh vùng.
Nếu khối try chạy thành công mà không ném lỗi, khối catch bị bỏ qua hoàn toàn. Nó chỉ dành cho nhánh thất bại.
Đối tượng lỗi
Bất cứ thứ gì bị ném ra đều được gán cho tên tham số trong catch (...). Thường đó là một instance của Error với ba trường hữu ích:
name cho biết lớp con (TypeError, RangeError, SyntaxError, v.v.; bài tiếp theo sẽ nói kỹ hơn). message là mô tả dễ đọc. stack là toàn bộ dấu vết ngăn xếp (stack trace), cực kỳ quý giá khi gỡ lỗi.
Một điểm cần lưu ý: JavaScript cho phép bạn throw bất cứ thứ gì, không chỉ đối tượng Error. Code cũ đôi khi viết throw "something broke". Khi tự viết throw, hãy luôn ném ra một Error để nơi gọi có được stack trace:
finally luôn chạy dù thế nào
finally là khối thứ ba, không bắt buộc, chạy dù có lỗi bị ném ra hay không, và dù catch có xử lý nó hay không. Nó dành cho việc dọn dẹp: đóng tệp, giải phóng khóa, ẩn biểu tượng đang tải:
Biểu tượng đang tải được ẩn đi dù việc tải thành công hay thất bại. Nếu không có finally, bạn sẽ phải viết dòng đó ở cả hai nhánh, và quên mất ở một nhánh.
finally vẫn chạy kể cả khi khối try hoặc catch có return. Hàm trả về sau khi finally chạy xong. Điều này đôi khi gây bất ngờ, nhưng phần lớn đó đúng là điều bạn muốn.
Không phải lúc nào cũng cần catch
catch là không bắt buộc. Một cặp try/finally đơn thuần là hợp lệ và hữu ích khi bạn muốn chắc chắn việc dọn dẹp diễn ra nhưng không định xử lý lỗi, mà muốn nó tiếp tục lan lên:
Khối try/finally bên trong giải phóng khóa ngay cả khi fn() ném lỗi, nhưng không nuốt mất lỗi: nơi gọi vẫn thấy nó. Lặng lẽ nuốt lỗi ("hỏng rồi mà không báo ai") là một trong những cơn ác mộng gỡ lỗi phổ biến nhất.
Ném lại lỗi: xử lý một số, chuyển phần còn lại lên trên
Khối catch không cần xử lý mọi thứ. Bạn có thể xem xét lỗi, xử lý những gì hợp lý, rồi ném lại phần còn lại:
Kiểm tra instanceof chính là mẫu cần nhớ: nhận diện những lỗi bạn biết cách khôi phục, và để mọi thứ khác tiếp tục lan lên call stack. Nuốt mọi lỗi bằng một khối catch rỗng là dấu hiệu code có vấn đề: bạn mất hết tín hiệu khi có điều bất ngờ xảy ra.
try/catch với async/await
Bên trong một hàm async, các promise bị reject mà bạn await sẽ biến thành lỗi được ném ra, và try/catch xử lý chúng y như lỗi đồng bộ:
Một điểm tinh tế: bạn phải await promise bên trong khối try. Nếu bạn trả về một promise mà không await, việc reject xảy ra sau khi hàm đã thoát, và catch không bao giờ thấy nó:
async function bad() {
try {
return fetch("/broken"); // no await - caller sees the rejection
} catch (err) {
// never runs
}
}
Quy tắc chung: trong hàm async, hãy await thứ mà bạn muốn try/catch bao quát.
try/catch lồng nhau
Bạn có thể lồng các khối try/catch khi code bên trong và bên ngoài thất bại vì những lý do khác nhau mà bạn muốn xử lý khác nhau:
Khối catch bên trong xử lý trường hợp "dữ liệu sai cấu trúc" bằng cách trả về một giá trị mặc định an toàn. Khối bên ngoài xử lý trường hợp "đầu vào hoàn toàn không phải JSON" bằng cách bọc lại rồi ném tiếp. Lồng nhau là ổn khi mỗi tầng có một chiến lược khôi phục riêng; nếu cả hai khối làm cùng một việc, hãy làm phẳng chúng.
Khi nào không nên dùng try/catch
try/catch là công cụ cho những lỗi có thể lường trước và khôi phục được. Nó không phải cách để che đậy bug.
- Đừng bọc toàn bộ thân hàm "cho chắc". Nếu không có kế hoạch thực sự cho lỗi đó, hãy để nó lan lên: một lỗi không bị bắt kèm stack trace hữu ích hơn một lỗi im lặng.
- Đừng dùng nó để điều khiển luồng chương trình. Khối
trycó chi phí thực sự và làm code rối hơn so với một phép kiểm traif.if (user)tốt hơntry { user.name } catch {}. - Đừng bắt lỗi rồi chỉ ghi log và bỏ qua. Ít nhất hãy ném lại, hoặc trả về một giá trị đặc biệt mà nơi gọi có thể nhận ra.
Phép thử trong đầu: "người dùng đoạn code này sẽ làm gì khi nó thất bại?" Nếu bạn chưa có câu trả lời, bạn chưa sẵn sàng để catch lỗi đó.
Tóm tắt nhanh
try { ... } catch (err) { ... }: chặn bắt các lỗi bị ném ra.finally { ... }: luôn chạy; dùng để dọn dẹp.throw new Error("..."): luôn ném các lớp con củaErrorđể có stack trace.throw err;bên trongcatch: ném lại khi bạn không xử lý được.awaitbên trongtry: bắt buộc đểtry/catchthấy được các rejection bất đồng bộ.
Tiếp theo: các kiểu lỗi
TypeError, RangeError, SyntaxError: JavaScript có cả một họ lớp lỗi dựng sẵn, và biết lớp nào mang ý nghĩa gì giúp việc bắt và báo lỗi chính xác hơn nhiều. Đó là nội dung của bài tiếp theo.
Câu hỏi thường gặp
try/catch hoạt động thế nào trong JavaScript?
Đặt đoạn code có rủi ro vào trong try { ... }. Nếu bất kỳ thứ gì bên trong ném ra lỗi, chương trình nhảy thẳng tới khối catch (err) { ... } với giá trị bị ném được gán cho err. Nếu không có lỗi, khối catch bị bỏ qua. Khối finally { ... } (không bắt buộc) luôn chạy dù có lỗi hay không, rất hữu ích để dọn dẹp.
Khi nào nên dùng try/catch trong JavaScript?
Dùng nó cho những thao tác thật sự có thể thất bại lúc chạy: JSON.parse với đầu vào không đáng tin, response của fetch, đọc ghi tệp hoặc mạng. Đừng bọc mọi dòng code: nếu bạn không có kế hoạch khôi phục, hãy để lỗi lan lên trên. Một try/catch bọc rộng quanh code đang chạy tốt chỉ che giấu bug chứ không xử lý chúng.
try/catch có bắt được lỗi bất đồng bộ không?
Chỉ khi bạn await promise bên trong khối try. Một lời gọi somePromise() đơn thuần sẽ không bị bắt: lỗi trở thành một unhandled rejection. Với async/await, try/catch hoạt động giống như với code đồng bộ. Với promise thuần, hãy dùng .catch() trên chuỗi promise.
Làm sao ném lại lỗi trong JavaScript?
Bên trong catch, chỉ cần throw err; (hoặc throw một lỗi mới bọc lỗi cũ). Cách này hữu ích khi bạn muốn xử lý một số lỗi và để các lỗi khác tiếp tục lan lên: kiểm tra kiểu hoặc thông điệp của lỗi trước, xử lý những gì bạn có thể, rồi ném lại phần còn lại để nơi gọi phía trên vẫn thấy chúng.