Menangani beberapa tangkapan dalam rantai janji

125

Saya masih cukup baru dalam promise dan saya menggunakan bluebird saat ini, namun saya memiliki skenario di mana saya tidak begitu yakin bagaimana cara terbaik untuk menghadapinya.

Jadi misalnya saya memiliki rantai janji dalam aplikasi ekspres seperti:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

Jadi perilaku yang saya kejar adalah:

  • Pergi untuk mendapatkan akun dengan Id
  • Jika ada penolakan pada saat ini, bom dan kembalikan kesalahan
  • Jika tidak ada kesalahan, ubah dokumen dikembalikan ke model
  • Verifikasi kata sandi dengan dokumen database
  • Jika kata sandi tidak cocok maka bom dan kembalikan kesalahan yang berbeda
  • Jika tidak ada kesalahan, ubah kata sandi
  • Kemudian kembalikan kesuksesan
  • Jika ada yang salah, kembalikan 500

Jadi saat ini tangkapan tampaknya tidak menghentikan rantai, dan itu masuk akal, jadi saya bertanya-tanya apakah ada cara bagi saya untuk memaksa rantai berhenti pada titik tertentu berdasarkan kesalahan, atau jika ada cara yang lebih baik untuk menyusun ini untuk mendapatkan beberapa bentuk perilaku percabangan, seperti halnya kasus if X do Y else Z.

Bantuan apa pun akan sangat bagus.

Untung
sumber
Bisakah Anda melempar ulang atau kembali lebih awal?
Pieter21

Jawaban:

126

Perilaku ini persis seperti lemparan sinkron:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

Itu setengah dari poin .catch- untuk dapat pulih dari kesalahan. Mungkin diinginkan untuk memutar ulang untuk memberi sinyal bahwa status masih error:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

Namun, ini saja tidak akan berfungsi dalam kasus Anda karena kesalahan ditangkap oleh penangan selanjutnya. Masalah sebenarnya di sini adalah bahwa penanganan kesalahan "HANDLE ANYTHING" yang digeneralisasi adalah praktik yang buruk secara umum dan sangat disukai dalam bahasa dan ekosistem pemrograman lain. Untuk alasan ini Bluebird menawarkan hasil tangkapan dengan tipe dan predikat.

Keuntungan tambahannya adalah logika bisnis Anda tidak (dan tidak seharusnya) harus mengetahui siklus permintaan / respons sama sekali. Ini bukan tanggung jawab kueri untuk memutuskan status dan kesalahan HTTP mana yang didapat klien dan kemudian saat aplikasi Anda berkembang, Anda mungkin ingin memisahkan logika bisnis (cara menanyakan DB Anda dan cara memproses data Anda) dari apa yang Anda kirim ke klien (kode status http apa, teks apa dan tanggapan apa).

Inilah cara saya menulis kode Anda.

Pertama, saya akan .Querymelempar NoSuchAccountError, saya akan membuat subkelas darinyaPromise.OperationalError mana Bluebird sudah menyediakannya. Jika Anda tidak yakin cara membuat subkelas kesalahan, beri tahu saya.

Saya juga akan membuat subkelasnya AuthenticationErrordan kemudian melakukan sesuatu seperti:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

Seperti yang Anda lihat - ini sangat bersih dan Anda dapat membaca teks seperti instruksi manual tentang apa yang terjadi dalam proses tersebut. Itu juga terpisah dari request / respon.

Sekarang, saya akan menyebutnya dari penangan rute seperti:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

Dengan cara ini, logikanya ada di satu tempat dan keputusan tentang cara menangani error ke klien ada di satu tempat dan tidak saling mengacaukan.

Benjamin Gruenbaum
sumber
11
Anda mungkin ingin menambahkan bahwa alasan memiliki .catch(someSpecificError)penangan perantara untuk beberapa kesalahan tertentu adalah jika Anda ingin menangkap jenis kesalahan tertentu (yang tidak berbahaya), tangani dan lanjutkan alur yang mengikutinya. Misalnya, saya memiliki beberapa kode startup yang memiliki urutan hal-hal yang harus dilakukan. Hal pertama adalah membaca file konfigurasi dari disk, tetapi jika file konfigurasi itu tidak ada, itu adalah kesalahan OK (program telah dibangun secara default) sehingga saya dapat menangani kesalahan khusus itu dan melanjutkan sisa aliran. Mungkin juga ada pembersihan lebih baik tidak pergi sampai nanti.
jfriend00
1
Saya berpikir bahwa "Itu setengah dari poin .catch - untuk dapat pulih dari kesalahan" membuatnya jelas tetapi terima kasih telah mengklarifikasi lebih lanjut, itu adalah contoh yang bagus.
Benjamin Gruenbaum
1
Bagaimana jika bluebird tidak digunakan? Promise es6 biasa hanya memiliki pesan kesalahan string yang diteruskan ke catch.
tukang jam
3
@clocksmith dengan ES6 menjanjikan Anda terjebak menangkap semuanya dan melakukan instanceofchceks secara manual.
Benjamin Gruenbaum
1
Bagi mereka yang mencari referensi untuk subclassing objek Error, baca bluebirdjs.com/docs/api/catch.html#filtered-catch . Artikel juga cukup banyak mereproduksi jawaban tangkap ganda yang diberikan di sini.
mummybot
47

.catchberfungsi seperti try-catchpernyataan, yang berarti Anda hanya perlu satu tangkapan di akhir:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });
Esailija
sumber
1
Ya, saya tahu tentang ini, tetapi saya tidak ingin melakukan rantai kesalahan yang besar, dan tampaknya lebih mudah dibaca saat melakukannya saat dan saat diperlukan. Oleh karena itu menangkap semua di akhir, tapi saya suka gagasan kesalahan ketik karena itu lebih deskriptif untuk maksudnya.
Grofit
8
@ Untung untuk apa nilainya - tangkapan yang diketik di Bluebird adalah ide Petka (Esailija) untuk memulai :) Tidak perlu meyakinkannya bahwa mereka adalah pendekatan yang lebih disukai di sini. Saya pikir dia tidak ingin membingungkan Anda karena banyak orang di JS tidak terlalu paham dengan konsep tersebut.
Benjamin Gruenbaum
17

Saya bertanya-tanya apakah ada cara bagi saya untuk memaksa rantai berhenti pada titik tertentu berdasarkan kesalahan

Tidak. Anda tidak dapat benar-benar "mengakhiri" rantai, kecuali jika Anda membuat pengecualian yang menggelembung sampai akhirnya. Lihat jawaban Benjamin Gruenbaum untuk mengetahui cara melakukannya.

Penurunan polanya bukan untuk membedakan jenis kesalahan, tetapi menggunakan kesalahan yang dimiliki statusCodedan bodybidang yang dapat dikirim dari satu .catchpenangan umum . Bergantung pada struktur aplikasi Anda, solusinya mungkin lebih bersih.

atau jika ada cara yang lebih baik untuk menyusunnya untuk mendapatkan beberapa bentuk perilaku percabangan

Ya, Anda dapat melakukan percabangan dengan promise . Namun, ini berarti meninggalkan rantai dan "kembali" ke bersarang - seperti yang Anda lakukan dalam pernyataan bertingkat if-else atau coba-tangkap:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});
Bergi
sumber
5

Saya telah melakukan cara ini:

Anda meninggalkan tangkapan Anda pada akhirnya. Dan lemparkan saja kesalahan saat terjadi di tengah-tengah rantai Anda.

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

Fungsi Anda yang lain mungkin akan terlihat seperti ini:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}
Leo Leao
sumber
4

Mungkin agak terlambat ke pesta, tapi mungkin saja bersarang .catch seperti yang ditunjukkan di sini:

Jaringan Pengembang Mozilla - Menggunakan Janji

Sunting: Saya mengirimkan ini karena menyediakan fungsionalitas yang diminta secara umum. Namun tidak dalam kasus khusus ini. Karena seperti yang sudah dijelaskan secara rinci oleh orang lain, .catchseharusnya memulihkan kesalahan. Anda tidak dapat, misalnya, mengirim respons ke klien dalam beberapa .catch callback karena a .catchwith no eksplisit return menyelesaikannya dengan undefineddalam kasus itu, menyebabkan proses .thendipicu meskipun rantai Anda tidak benar-benar terselesaikan, berpotensi menyebabkan yang berikut ini .catchmemicu dan mengirim respons lain kepada klien, menyebabkan kesalahan dan kemungkinan besar akan mengganggu UnhandledPromiseRejectionAnda. Saya harap kalimat yang berbelit-belit ini masuk akal bagi Anda.

denkquer
sumber
1
@Anda benar. Saya memperluas jawaban saya, menjelaskan mengapa perilaku yang diinginkannya masih tidak mungkin dilakukan dengan bersarang
denkquer
2

Sebaliknya .then().catch()...Anda bisa melakukannya .then(resolveFunc, rejectFunc). Rantai janji ini akan lebih baik jika Anda menangani berbagai hal di sepanjang jalan. Inilah cara saya menulis ulang:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

Catatan: Ini if (error != null)adalah sedikit retasan untuk berinteraksi dengan kesalahan terbaru.

mvndaai
sumber
1

Saya pikir jawaban Benjamin Gruenbaum di atas adalah solusi terbaik untuk rangkaian logika yang kompleks, tetapi berikut adalah alternatif saya untuk situasi yang lebih sederhana. Saya hanya menggunakan sebuah errorEncounteredbendera bersama dengan return Promise.reject()untuk melewati pernyataan thenatau berikutnya catch. Jadi akan terlihat seperti ini:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

Jika Anda memiliki lebih dari dua pasang maka / tangkap, Anda mungkin harus menggunakan solusi Benjamin Gruenbaum. Tapi ini berfungsi untuk pengaturan sederhana.

Perhatikan bahwa final catchhanya memiliki return;daripada return Promise.reject();, karena tidak ada yang berikutnya thenyang perlu kita lewati, dan itu akan dihitung sebagai penolakan Promise yang tidak tertangani, yang tidak disukai Node. Seperti yang tertulis di atas, final catchakan mengembalikan Janji yang diselesaikan dengan damai.

temporary_user_name
sumber