Mengapa saya tidak bisa memasukkan penangan Promise.catch?

127

Mengapa saya tidak bisa begitu saja melempar Errorpanggilan balik tangkapan dan membiarkan proses menangani kesalahan seolah-olah itu dalam lingkup lain?

Jika saya tidak melakukan console.log(err)apa-apa akan dicetak dan saya tidak tahu apa-apa tentang apa yang terjadi. Prosesnya baru saja berakhir ...

Contoh:

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
});

Jika panggilan balik dieksekusi di utas utama, mengapa Errorpenelepon ditelan oleh lubang hitam?

demian85
sumber
11
Itu tidak tertelan oleh lubang hitam. Ia menolak janji yang .catch(…)kembali.
Bergi
alih-alih .catch((e) => { throw new Error() }), menulis .catch((e) => { return Promise.reject(new Error()) })atau hanya.catch((e) => Promise.reject(new Error()))
chharvey
1
@chharvey semua cuplikan kode dalam komentar Anda memiliki perilaku yang persis sama, kecuali yang awal jelas paling jelas.
Сергей Гринько

Jawaban:

157

Seperti yang orang lain jelaskan, "lubang hitam" adalah karena melempar ke .catchdalam rantai yang berlanjut dengan janji yang ditolak, dan Anda tidak memiliki tangkapan lagi, yang mengarah ke rantai yang tidak tertembus, yang menelan kesalahan (buruk!)

Tambahkan satu tangkapan lagi untuk melihat apa yang terjadi:

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!
});

Tangkapan di tengah rantai berguna ketika Anda ingin rantai melanjutkan meskipun langkah gagal, tetapi melempar kembali berguna untuk terus gagal setelah melakukan hal-hal seperti logging informasi atau langkah pembersihan, bahkan mungkin mengubah kesalahan mana terlempar.

Menipu

Untuk membuat kesalahan muncul sebagai kesalahan di konsol web, seperti yang Anda maksudkan pada awalnya, saya menggunakan trik ini:

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

Bahkan nomor baris tetap ada, jadi tautan di konsol web membawa saya langsung ke file dan baris tempat kesalahan (asli) terjadi.

Mengapa ini berhasil?

Pengecualian dalam fungsi yang disebut sebagai pemenuhan janji atau penangan penolakan akan secara otomatis dikonversi menjadi penolakan atas janji yang seharusnya Anda kembalikan. Kode janji yang menyebut fungsi Anda menangani hal ini.

Fungsi yang disebut oleh setTimeout di sisi lain, selalu berjalan dari kondisi stabil JavaScript, yaitu berjalan dalam siklus baru di loop acara JavaScript. Pengecualian ada tidak tertangkap oleh apa pun, dan membuatnya ke konsol web. Karena errmenyimpan semua informasi tentang kesalahan, termasuk tumpukan asli, file dan nomor baris, masih dilaporkan dengan benar.

jib
sumber
3
Jib, itu trik yang menarik, bisakah kau membantuku memahami mengapa itu berhasil?
Brian Keith
8
Tentang trik itu: Anda melempar karena ingin masuk, jadi mengapa tidak langsung masuk? Trik ini akan secara acak melemparkan kesalahan yang tidak dapat ditandingi .... Tetapi seluruh gagasan tentang pengecualian (dan cara menjanjikannya) adalah menjadikan penelepon bertanggung jawab untuk menangkap kesalahan dan menanganinya. Kode ini secara efektif membuat penelepon tidak mungkin berurusan dengan kesalahan. Mengapa tidak membuat fungsi untuk menanganinya untuk Anda? function logErrors(e){console.error(e)}lalu gunakan seperti do1().then(do2).catch(logErrors). Jawabannya sendiri bagus, +1
Stijn de Witt
3
@ jib Saya menulis lambda AWS yang berisi banyak janji yang terhubung kurang lebih seperti dalam kasus ini. Untuk mengeksploitasi alarm dan pemberitahuan AWS jika terjadi kesalahan, saya harus membuat crash lambda membuat kesalahan (saya kira). Apakah trik satu-satunya cara untuk mendapatkan ini?
masciugo
2
@StijndeWitt Dalam kasus saya, saya mencoba mengirim detail kesalahan ke server saya di window.onerrorevent handler. Hanya dengan melakukan setTimeouttrik ini dapat dilakukan. Kalau tidak, tidak window.onerrorakan pernah mendengar apa pun tentang kesalahan yang terjadi di Janji.
hudidit
1
@hudidit Masih, apakah itu console.logatau postErrorToServer, Anda bisa melakukan apa yang perlu dilakukan. Tidak ada alasan kode apa pun yang ada di window.onerrordalamnya tidak dapat difaktorkan ke dalam fungsi terpisah dan dipanggil dari 2 tempat. Mungkin lebih pendek dari setTimeoutgaris.
Stijn de Witt
46

Hal-hal penting untuk dipahami di sini

  1. Baik thendan catchfungsi mengembalikan objek janji baru.

  2. Entah melempar atau menolak secara eksplisit, akan memindahkan janji saat ini ke negara yang ditolak.

  3. Karena thendan catchmengembalikan objek janji baru, mereka dapat dirantai.

  4. Jika Anda melempar atau menolak di dalam handler janji ( thenatau catch), itu akan ditangani di handler penolakan berikutnya di jalur rantai.

  5. Seperti yang disebutkan oleh jfriend00, thendan catchpenangan tidak dieksekusi secara bersamaan. Ketika pawang melempar, itu akan segera berakhir. Jadi, tumpukan akan dibatalkan dan pengecualian akan hilang. Itu sebabnya melempar pengecualian menolak janji saat ini.


Dalam kasus Anda, Anda menolak di dalam do1dengan melempar Errorobjek. Sekarang, janji saat ini akan dalam keadaan ditolak dan kontrol akan ditransfer ke penangan berikutnya, yang thendalam kasus kami.

Karena thenhandler tidak memiliki handler penolakan, maka handler do2tidak akan dieksekusi sama sekali. Anda dapat mengkonfirmasi ini dengan menggunakan console.logdi dalamnya. Karena janji saat ini tidak memiliki penangan penolakan, itu juga akan ditolak dengan nilai penolakan dari janji sebelumnya dan kontrol akan ditransfer ke penangan berikutnya yang catch.

Seperti catchpenangan penolakan, ketika Anda melakukannya console.log(err.stack);di dalamnya, Anda dapat melihat jejak tumpukan kesalahan. Sekarang, Anda melempar Errorobjek dari itu sehingga janji yang dikembalikan oleh catchjuga akan dalam keadaan ditolak.

Karena Anda belum memasang penangan penolakan apa pun pada catch, Anda tidak dapat mengamati penolakan tersebut.


Anda dapat membagi rantai dan memahami ini dengan lebih baik, seperti ini

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);
});

Output yang akan Anda dapatkan akan seperti

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

Di dalam catchhandler 1, Anda mendapatkan nilai promiseobjek sebagai ditolak.

Cara yang sama, janji yang dikembalikan oleh catchpawang 1, juga ditolak dengan kesalahan yang sama dengan yang promiseditolak dan kami mengamatinya di catchpawang kedua .

thethourtheye
sumber
3
Mungkin juga layak menambahkan bahwa .then()penangan adalah async (tumpukan dibatalkan sebelum dieksekusi) sehingga pengecualian di dalamnya harus diubah menjadi penolakan, jika tidak akan ada penangan pengecualian untuk menangkapnya.
jfriend00
7

Saya mencoba setTimeout()metode yang dijelaskan di atas ...

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

Mengganggu, saya menemukan ini benar-benar tidak dapat diuji. Karena itu melempar kesalahan asinkron, Anda tidak dapat membungkusnya di dalam try/catchpernyataan, karena catchakan berhenti mendengarkan pada saat kesalahan dilemparkan.

Saya kembali ke hanya menggunakan pendengar yang bekerja dengan sempurna dan, karena itu JavaScript harus digunakan, sangat dapat diuji.

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

    /* Throw so the promise is still rejected for testing */
    throw err;
});
RiggerTheGeek
sumber
3
Jest memiliki pengatur waktu yang harus menangani situasi ini.
jordanbtucker
2

Menurut spec (lihat 3.III.d) :

d. Jika menelepon maka lontarkan pengecualian e,
  a. Jika resolPromise atau rejectPromise telah dipanggil, abaikan saja.
  b. Kalau tidak, tolak janji dengan e sebagai alasannya.

Itu berarti bahwa jika Anda melempar pengecualian dalam thenfungsi, itu akan ditangkap dan janji Anda akan ditolak. catchtidak masuk akal di sini, itu hanya jalan pintas ke.then(null, function() {})

Saya kira Anda ingin mencatat penolakan yang tidak ditangani dalam kode Anda. Kebanyakan perpustakaan menjanjikan api unhandledRejectionuntuk itu. Inilah intisari yang relevan dengan diskusi tentangnya.

adil-boris
sumber
Patut disebutkan bahwa unhandledRejectionpengait adalah untuk JavaScript sisi server, di sisi klien, peramban yang berbeda memiliki solusi yang berbeda. KAMI belum menstandarkannya tetapi sudah sampai di sana perlahan tapi pasti.
Benjamin Gruenbaum
1

Saya tahu ini agak terlambat, tetapi saya menemukan utas ini, dan tidak ada solusi yang mudah diimplementasikan untuk saya, jadi saya membuat sendiri:

Saya menambahkan fungsi pembantu kecil yang mengembalikan janji, seperti:

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

Kemudian, jika saya memiliki tempat tertentu di rantai janji saya di mana saya ingin melemparkan kesalahan (dan menolak janji), saya hanya kembali dari fungsi di atas dengan kesalahan yang saya buat, seperti:

}).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);
})

Dengan cara ini saya bisa mengendalikan kesalahan tambahan dari dalam rantai janji. Jika Anda ingin juga menangani kesalahan janji 'normal', Anda akan memperluas tangkapan Anda untuk memperlakukan kesalahan 'dilempar sendiri' secara terpisah.

Semoga ini bisa membantu, ini adalah jawaban stackoverflow pertama saya!

nuudles
sumber
Promise.reject(error)alih-alih new Promise(function (resolve, reject){ reject(error) })(yang tetap membutuhkan pernyataan pengembalian)
Funkodebat
0

Ya menjanjikan kesalahan menelan, dan Anda hanya bisa menangkapnya .catch, seperti yang dijelaskan lebih detail dalam jawaban lain. Jika Anda berada di Node.js dan ingin mereproduksi throwperilaku normal , mencetak jejak tumpukan ke konsol dan keluar dari proses, Anda dapat melakukan

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Jesús Carrera
sumber
1
Tidak, itu tidak cukup, karena Anda harus meletakkannya di akhir setiap rantai janji yang Anda miliki. Agak mengaitkan pada unhandledRejectionacara
Bergi
Ya, itu dengan asumsi bahwa Anda mengaitkan janji-janji Anda sehingga jalan keluar adalah fungsi terakhir dan tidak dikejar. Acara yang Anda sebutkan saya pikir itu hanya jika menggunakan Bluebird.
Jesús Carrera
Bluebird, Q, kapan, asli menjanjikan, ... Itu kemungkinan akan menjadi standar.
Bergi