Tại sao tôi không thể ném vào bên trong trình xử lý Promise.catch?


127

Tại sao tôi không thể ném Error cuộc gọi lại bên trong và để quá trình xử lý lỗi như thể nó ở bất kỳ phạm vi nào khác?

Nếu tôi không làm console.log(err)gì thì không được in ra và tôi không biết gì về những gì đã xảy ra. Quá trình chỉ kết thúc ...

Thí dụ:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

Nếu các cuộc gọi lại được thực thi trong luồng chính, tại sao Errorbị lỗ đen nuốt chửng?


11
Nó không bị nuốt bởi một lỗ đen. Nó từ chối lời hứa mà .catch(…)trở về.
Bergi


thay vì .catch((e) => { throw new Error() }), viết .catch((e) => { return Promise.reject(new Error()) })hoặc đơn giản.catch((e) => Promise.reject(new Error()))
chharvey

1
@chharvey tất cả các đoạn mã trong bình luận của bạn có hành vi giống hệt nhau, ngoại trừ đoạn mã ban đầu rõ ràng là rõ ràng nhất.
Hôm nay,

Câu trả lời:


157

Như những người khác đã giải thích, "lỗ đen" là do ném vào bên trong .catch chuỗi tiếp tục với một lời hứa bị từ chối và bạn không còn bị bắt, dẫn đến một chuỗi bị phá hủy, nuốt lỗi (xấu!)

Thêm một lần bắt nữa để xem những gì đang xảy ra:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

Một cú bắt ở giữa một chuỗi là hữu ích khi bạn muốn chuỗi tiến hành mặc dù bước không thành công, nhưng ném lại rất hữu ích cho tiếp tục thất bại sau khi thực hiện những việc như đăng nhập thông tin hoặc các bước dọn dẹp, thậm chí có thể thay đổi lỗi nào được ném.

Lừa

Để làm cho lỗi hiển thị là một lỗi trong bảng điều khiển web, như bạn dự định ban đầu, tôi sử dụng mẹo này:

.catch(function(err) { setTimeout(function() { throw err; }); });

Ngay cả số dòng tồn tại, do đó, liên kết trong bảng điều khiển web đưa tôi đến thẳng tệp và dòng nơi xảy ra lỗi (bản gốc).

Tại sao nó hoạt động

Bất kỳ ngoại lệ nào trong hàm được gọi là trình xử lý từ chối hoặc thực hiện lời hứa sẽ tự động được chuyển đổi thành từ chối lời hứa mà bạn phải trả lại. Mã lời hứa gọi chức năng của bạn đảm nhận việc này.

Mặt khác, một hàm được gọi bởi setTimeout, luôn chạy từ trạng thái ổn định JavaScript, tức là nó chạy trong một chu kỳ mới trong vòng lặp sự kiện JavaScript. Các trường hợp ngoại lệ không bị bắt bởi bất cứ điều gì và đưa nó vào bảng điều khiển web. Vì errchứa tất cả thông tin về lỗi, bao gồm ngăn xếp ban đầu, tệp và số dòng, nên nó vẫn được báo cáo chính xác.


3
Jib, đó là một mẹo thú vị, bạn có thể giúp tôi hiểu tại sao nó hoạt động không?
Brian Keith

8
Về thủ thuật đó: bạn đang ném vì muốn đăng nhập, vậy tại sao không đăng nhập trực tiếp? Thủ thuật này vào một thời điểm 'ngẫu nhiên' sẽ gây ra một lỗi không thể khắc phục .... Nhưng toàn bộ ý tưởng về các trường hợp ngoại lệ (và cách hứa hẹn đối phó với chúng) là khiến cho người gọi có trách nhiệm bắt lỗi và xử lý lỗi đó. Mã này có hiệu quả làm cho người gọi không thể xử lý các lỗi. Tại sao không chỉ làm một chức năng để xử lý nó cho bạn? function logErrors(e){console.error(e)}sau đó sử dụng nó như do1().then(do2).catch(logErrors). Trả lời chính nó là btw tuyệt vời, +1
Stijn de Witt

3
@jib Tôi đang viết một lambda AWS chứa nhiều lời hứa được kết nối ít nhiều giống như trong trường hợp này. Để khai thác các báo động và thông báo AWS trong trường hợp có lỗi, tôi cần phải làm cho lambda gặp sự cố (tôi đoán). Là mẹo duy nhất để có được điều này?
masciugo

2
@StijndeWitt Trong trường hợp của tôi, tôi đã cố gắng gửi chi tiết lỗi đến máy chủ của mình trong window.onerrortrình xử lý sự kiện. Chỉ bằng cách thực hiện các setTimeoutmẹo này có thể được thực hiện. Nếu không window.onerrorsẽ không bao giờ nghe một điều về các lỗi xảy ra trong Promise.
hudidit

1
@hudidit Tuy nhiên, dù nó console.loghay postErrorToServer, bạn chỉ có thể làm những gì cần phải làm. Không có lý do gì mà bất kỳ mã nào trong window.onerrorkhông thể được đưa vào một hàm riêng biệt và được gọi từ 2 vị trí. Nó thậm chí có thể ngắn hơn setTimeoutdòng.
Stijn de Witt

46

Những điều quan trọng cần hiểu ở đây

  1. Cả hai thenvà các catchhàm trả về các đối tượng lời hứa mới.

  2. Hoặc ném hoặc từ chối rõ ràng, sẽ chuyển lời hứa hiện tại sang trạng thái bị từ chối.

  3. Kể từ khi thencatchtrả lại các đối tượng lời hứa mới, chúng có thể bị xiềng xích.

  4. Nếu bạn ném hoặc từ chối bên trong một trình xử lý lời hứa ( thenhoặc catch), nó sẽ được xử lý trong trình xử lý từ chối tiếp theo xuống đường dẫn chuỗi.

  5. Như jfriend00 đã đề cập, trình xử lý thencatchtrình xử lý không được thực thi đồng bộ. Khi một người xử lý ném, nó sẽ kết thúc ngay lập tức. Vì vậy, ngăn xếp sẽ được mở ra và ngoại lệ sẽ bị mất. Đó là lý do tại sao ném một ngoại lệ từ chối lời hứa hiện tại.


Trong trường hợp của bạn, bạn đang từ chối bên trong do1bằng cách ném một Errorvật thể. Bây giờ, lời hứa hiện tại sẽ ở trạng thái bị từ chối và quyền kiểm soát sẽ được chuyển sang xử lý tiếp theo, đó là thentrong trường hợp của chúng tôi.

thentrình xử lý không có trình xử lý từ chối, nên do2sẽ không được thực thi. Bạn có thể xác nhận điều này bằng cách sử dụng console.logbên trong nó. Vì lời hứa hiện tại không có trình xử lý từ chối, nên nó cũng sẽ bị từ chối với giá trị từ chối từ lời hứa trước đó và quyền kiểm soát sẽ được chuyển sang trình xử lý tiếp theo catch.

Như catchlà một trình xử lý từ chối, khi bạn thực hiện console.log(err.stack);bên trong nó, bạn có thể thấy dấu vết ngăn xếp lỗi. Bây giờ, bạn đang ném một Errorđối tượng từ nó để lời hứa được trả lại catchcũng sẽ ở trạng thái bị từ chối.

Vì bạn chưa đính kèm bất kỳ trình xử lý từ chối nào vào catch, nên bạn không thể quan sát từ chối.


Bạn có thể chia chuỗi và hiểu điều này tốt hơn, như thế này

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

Đầu ra bạn sẽ nhận được sẽ giống như

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Trong catchtrình xử lý 1, bạn sẽ nhận được giá trị của promiseđối tượng là bị từ chối.

Tương tự, lời hứa được trả về bởi catchtrình xử lý 1, cũng bị từ chối với cùng một lỗi promisebị từ chối và chúng tôi đang quan sát nó trong catchtrình xử lý thứ hai .


3
Cũng có thể đáng để thêm rằng các .then()trình xử lý không đồng bộ (ngăn xếp không được xử lý trước khi chúng được thực thi) vì vậy các ngoại lệ bên trong chúng phải được chuyển thành từ chối, nếu không sẽ không có trình xử lý ngoại lệ nào để bắt chúng.
jfriend00

7

Tôi đã thử setTimeout()phương pháp chi tiết ở trên ...

.catch(function(err) { setTimeout(function() { throw err; }); });

Khó chịu, tôi thấy điều này là hoàn toàn không thể kiểm chứng. Bởi vì nó gây ra lỗi không đồng bộ, bạn không thể gói nó trong một try/catchcâu lệnh, bởi vì ý catchchí đã ngừng lắng nghe bởi lỗi thời gian được đưa ra.

Tôi trở lại chỉ sử dụng một trình nghe hoạt động hoàn hảo và, bởi vì đó là cách sử dụng JavaScript, rất có thể kiểm tra được.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});

3
Jest có đồng hồ bấm giờ nên xử lý tình huống này.
jordanbtucker

2

Theo thông số kỹ thuật (xem 3.III.d) :

d. Nếu gọi thì ném ngoại lệ e,
  a. Nếu notifyPromise hoặc từ chốiPromise đã được gọi, hãy bỏ qua nó.
  b. Nếu không, từ chối lời hứa với e là lý do.

Điều đó có nghĩa là nếu bạn ném ngoại lệ vào thenchức năng, nó sẽ bị bắt và lời hứa của bạn sẽ bị từ chối. catchđừng có ý nghĩa ở đây, nó chỉ là lối tắt để.then(null, function() {})

Tôi đoán bạn muốn đăng nhập các từ chối chưa được xử lý trong mã của bạn. Hầu hết các thư viện hứa hẹn sẽ sa thảiunhandledRejection cho nó. Đây là ý chính có liên quan với các cuộc thảo luận về nó.


Điều đáng nói là unhandledRejectionhook dành cho JavaScript phía máy chủ, ở phía máy khách, các trình duyệt khác nhau có các giải pháp khác nhau. CHÚNG TÔI chưa chuẩn hóa nó nhưng nó đến đó từ từ nhưng chắc chắn.
Benjamin Gruenbaum

1

Tôi biết điều này hơi muộn, nhưng tôi đã xem qua chủ đề này và không có giải pháp nào dễ thực hiện cho tôi, vì vậy tôi đã tự mình nghĩ ra:

Tôi đã thêm một chức năng trợ giúp nhỏ trả lại một lời hứa, như vậy:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Sau đó, nếu tôi có một vị trí cụ thể trong bất kỳ chuỗi lời hứa nào của tôi, nơi tôi muốn đưa ra một lỗi (và từ chối lời hứa), tôi chỉ cần trả về từ hàm trên với lỗi được xây dựng của mình, như vậy:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

Bằng cách này, tôi kiểm soát được việc ném thêm lỗi từ bên trong chuỗi hứa hẹn. Nếu bạn cũng muốn xử lý các lỗi hứa 'bình thường', bạn sẽ mở rộng khả năng bắt để xử lý riêng các lỗi 'tự ném'.

Hy vọng điều này sẽ giúp, đó là câu trả lời stackoverflow đầu tiên của tôi!


Promise.reject(error)thay vì new Promise(function (resolve, reject){ reject(error) })(dù sao cũng cần một tuyên bố trở lại)
Funkodebat

0

Có hứa hẹn lỗi nuốt và bạn chỉ có thể bắt lỗi .catch, như được giải thích chi tiết hơn trong các câu trả lời khác. Nếu bạn đang ở trong Node.js và muốn tái tạo throwhành vi bình thường , in dấu vết ngăn xếp lên bàn điều khiển và quá trình thoát, bạn có thể làm

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});

1
Không, điều đó là không đủ, vì bạn sẽ cần phải đặt nó vào cuối mỗi chuỗi hứa hẹn mà bạn có. Thay vào đó unhandledRejectionsự kiện
Bergi 7/11/2015

Vâng, đó là giả sử rằng bạn xâu chuỗi lời hứa của mình để lối ra là chức năng cuối cùng và nó không bị bắt sau đó. Sự kiện mà bạn đề cập tôi nghĩ chỉ khi sử dụng Bluebird.
Jesús Carrera

Bluebird, Q, khi, lời hứa bản địa, có thể nó sẽ trở thành một tiêu chuẩn.
Bergi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.