Xử lý lỗi với promise trong JavaScript

Cập nhật ngày 08/05/2022

Chuỗi promise rất hiệu quả trong việc xử lý lỗi. Khi một promise bị từ chối, luồng điều khiển sẽ chuyển đến phần xử lý từ chối gần nhất. Điều đó rất thuận tiện trong thực tế.

Ví dụ với đoạn code bên dưới, URL truyền vào hàm fetch là sai (không có trang web nào như vậy) và .catch xử lý lỗi:

fetch("https://no-such-server.blabla") // rejects vì URL bị sai
  .then((response) => response.json())
  .catch((err) => console.log(err)); // TypeError: failed to fetch

Như bạn thấy, .catch không nhất thiết phải có ngay lập tức mà có thể sau một hoặc vài lần .then.

Hoặc có thể, mọi thứ đều ổn với trang web, nhưng kết quả trả về là một JSON không hợp lệ. Khi đó, cách dễ nhất để bắt tất cả các lỗi là thêm .catch vào cuối chuỗi promise:

fetch("/api/user.json")
  .then((response) => response.json())
  .then((user) => fetch(`https://api.github.com/users/${user.name}`))
  .then((response) => response.json())
  .then(
    (githubUser) =>
      new Promise((resolve, reject) => {
        let img = document.createElement("img");
        img.src = githubUser.avatar_url;
        img.className = "promise-avatar-example";
        document.body.append(img);

        setTimeout(() => {
          img.remove();
          resolve(githubUser);
        }, 3000);
      })
  )
  .catch((error) => console.log(error.message));

Thông thường, .catch sẽ không kích hoạt. Nhưng nếu bất kỳ promise nào ở trên bị từ chối (sự cố mạng hoặc json không hợp lệ hoặc bất cứ điều gì) thì .catch sẽ bắt được lỗi đó.

Try...catch ẩn

Code thực thi của promise và các trình xử lý promise luôn có try..catch ẩn xung quanh. Nếu ngoại lệ (exception) xảy ra, lỗi đó sẽ bị bắt bởi .catch và đó coi như một sự từ chối.

Ví dụ đoạn code sau:

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(console.log); // Error: Whoops!

Hoạt động tương tự như sau:

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(console.log); // Error: Whoops!

Phần try..catch ẩn xung quanh trình thực thi sẽ tự động bắt lỗi và biến promise trở thành trạng thái bị từ chối.

Điều này xảy ra không chỉ trong hàm thực thi mà còn trong các trình xử lý của nó. Nếu bạn throw bên trong một trình xử lý .then, điều đó có nghĩa là một promise bị từ chối. Do đó, luồng điều khiển sẽ chuyển đến trình xử lý lỗi gần nhất - .catch gần nhất.

Sau đây là ví dụ:

new Promise((resolve, reject) => {
  resolve("ok");
})
  .then((result) => {
    throw new Error("Whoops!"); // từ chối promise
  })
  .catch(console.log); // Error: Whoops!

Điều này xảy ra với tất cả các lỗi, không chỉ những lỗi do câu lệnh throw. Ví dụ lỗi lập trình như sau:

new Promise((resolve, reject) => {
  resolve("ok");
})
  .then((result) => {
    aha(); // hàm chưa định nghĩa
  })
  .catch(console.log); // ReferenceError: aha is not defined

Tóm lại, .catch không chỉ bắt những lời từ chối promise cụ thể, mà còn những lỗi vô tình như trên.

Rethrowing khi có lỗi

Như bạn nhận thấy, .catch ở cuối chuỗi promise tương tự như try..catch. Mình có thể có nhiều trình xử lý .then tùy thích và sau đó sử dụng một trình xử lý .catch duy nhất ở cuối để bắt lỗi trong tất cả các trường hợp.

Ở cấu trúc try..catch thông thường, mình sẽ phân tích lỗi và throw ra lỗi nếu không thể xử lý được. Điều tương tự cũng xảy ra với promise.

Nếu bạn throw bên trong .catch thì luồng điều khiển sẽ chuyển đến trình xử lý lỗi gần nhất tiếp theo. Sau đó, giả sử bạn xử lý lỗi và kết thúc bình thường, thì luồng điều khiển sẽ tiếp tục chuyển đến .then gần nhất tiếp theo.

Trong ví dụ bên dưới, .catch xử lý lỗi thành công:

// thực hiện catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    console.log("The error is handled, continue normally");
  })
  .then(() => console.log("Next successful handler runs"));

Ở đây khối .catch kết thúc bình thường. Vì vậy, trình xử lý .then tiếp theo được gọi.

Ví dụ sau đây, bạn thấy trường hợp khác với .catch. Trình xử lý tại (*) bắt lỗi và không thể xử lý vì nó chỉ biết cách xử lý lỗi URIError. Vì vậy, throw error một lần nữa:

// thực hiện catch -> catch
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    // (*)
    if (error instanceof URIError) {
      // nếu lỗi là URIError thì xử lý nó
    } else {
      console.log("Can't handle such error");
      throw error; // lỗi không biết -> throw để xử lý ở catch tiếp
    }
  })
  .then(function () {
    // không chạy vào đây
  })
  .catch((error) => {
    // (**)
    console.log(`The unknown error has occurred: ${error}`);
    // don't return anything => execution goes the normal way
  });

Quá trình thực hiện sẽ nhảy từ bước .catch đầu tiên tại (*) sang lần tiếp theo tại (**).

Từ chối promise mà không xử lý

Điều gì xảy ra khi lỗi không được xử lý?

Ví dụ, mình quên thêm .catch vào cuối chuỗi promise, như sau:

new Promise(function () {
  noSuchFunction(); // lỗi hàm chưa định nghĩa
}).then(() => {
  // trình xử lý promise thành công
}); // không có .catch ở cuối

Trường hợp có lỗi, promise bị từ chối và việc thực thi sẽ chuyển đến .catch gần nhất. Nhưng không có. Vì vậy, lỗi bị kẹt, không có đoạn code nào xử lý lỗi.

Thực tế, giống như với các lỗi thông thường trong code, điều đó có nghĩa là một điều gì đó đã có lỗi sai nghiêm trọng.

Điều gì xảy ra khi một lỗi thường xuyên xuất hiện và không được bắt bởi try..catch?

Chương trình sẽ bị crash với một thông báo trong DevTool. Điều tương tự cũng xảy ra với những promise bị từ chối mà không được catch.

JavaScript engine theo dõi những từ chối promise như vậy và tạo ra lỗi toàn cục. Bạn có thể thấy trong console nếu chạy ví dụ trên.

Trong trình duyệt, bạn có thể thấy các lỗi như vậy bằng cách dùng sự kiện unhandledrejection:

window.addEventListener("unhandledrejection", function (event) {
  // object event có hai thuộc tính đặc biệt là promise và reason.
  console.log(event.promise); // [object Promise] - promise sinh ra lỗi
  console.log(event.reason); // Error: Whoops! - error object chưa được bắt
});

new Promise(function () {
  throw new Error("Whoops!");
}); // không có catch để bắt lỗi

Nếu lỗi xảy ra và không có .catch, trình xử lý unhandledrejection sẽ kích hoạt và lấy đối tượng event chứa thông tin về lỗi. Sau đó, bạn có thể làm điều gì đó.

Thông thường, những lỗi như vậy là không thể khôi phục được. Do đó, cách tốt nhất là thông báo cho người dùng về sự cố và có thể báo cáo sự cố cho máy chủ.

Trong các môi trường không phải trình duyệt như Node.js, có nhiều cách khác để theo dõi các lỗi chưa được khắc phục.

Tổng kết

  • .catch xử lý lỗi trong tất cả các loại promise: có thể là lời gọi reject() hoặc lỗi được đưa ra trong một trình xử lý.
  • Bạn nên đặt chính xác .catch tại những nơi mà bạn muốn xử lý lỗi và biết cách xử lý. Trình xử lý nên phân tích lỗi và throw ra những lỗi chưa biết.
  • Nếu không có cách nào để khôi phục sau lỗi, bạn có thể không cần dùng .catch.
  • Trong mọi trường hợp, bạn nên có trình xử lý sự kiện unhandledrejection (đối với trình duyệt và các môi trường tương tự khác) để theo dõi các lỗi chưa được khắc phục và thông báo cho người dùng (hoặc có thể là máy chủ của mình), để chương trình không bao giờ chết.

Tham khảo: Error handling with promises

★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé:

Kết thúc sớm Promise chaining trong JavaScript
Các promise API trong JavaScript
Chia sẻ:

Bình luận