Putuskan rantai janji dan panggil fungsi berdasarkan langkah dalam rantai yang memutuskannya (ditolak)

140

Memperbarui:

Untuk membantu pemirsa postingan ini di masa mendatang, saya membuat demo jawaban pluma ini .

Pertanyaan:

Tujuan saya tampaknya cukup mudah.

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

Masalahnya di sini adalah jika saya gagal pada langkah 1, stepError(1)DAN keduanya stepError(2)dipecat. Jika saya tidak return $q.rejectmaka stepError(2)tidak akan dipecat, tetapi step(2)akan, yang saya mengerti. Saya telah mencapai segalanya kecuali apa yang saya coba lakukan.

Bagaimana cara menulis promise sehingga saya bisa memanggil fungsi saat ditolak, tanpa memanggil semua fungsi di rantai error? Atau apakah ada cara lain untuk melakukannya?

Ini demo langsung jadi Anda punya sesuatu untuk dikerjakan.

Memperbarui:

Saya sepertinya telah menyelesaikannya. Di sini, saya menangkap kesalahan di akhir rantai dan meneruskan data reject(data)sehingga saya akan tahu masalah apa yang harus ditangani dalam fungsi kesalahan. Ini sebenarnya tidak memenuhi persyaratan saya karena saya tidak ingin bergantung pada data. Ini akan menjadi timpang, tetapi dalam kasus saya akan lebih bersih untuk melewatkan callback kesalahan ke fungsi daripada bergantung pada data yang dikembalikan untuk menentukan apa yang harus dilakukan.

Demo langsung di sini (klik).

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }
m59
sumber
1
Ada javascript lib asinkron yang mungkin membantu jika ini menjadi lebih rumit
lucuma
Promise.prototype.catch()contoh di MDN menunjukkan solusi untuk masalah yang sama persis.
toraritte

Jawaban:

203

Alasan kode Anda tidak berfungsi seperti yang diharapkan adalah karena kode tersebut melakukan sesuatu yang berbeda dari yang Anda kira.

Katakanlah Anda memiliki sesuatu seperti berikut:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

Untuk lebih memahami apa yang terjadi, anggaplah ini adalah kode sinkron dengan try/ catchblok:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

The onRejectedhandler (argumen kedua then) pada dasarnya adalah mekanisme koreksi kesalahan (seperti catchblok). Jika kesalahan dilemparkan handleErrorOne, itu akan ditangkap oleh blok catch berikutnya ( catch(e2)), dan seterusnya.

Ini jelas bukan yang Anda inginkan.

Katakanlah kita ingin seluruh rantai resolusi gagal tidak peduli apa yang salah:

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Catatan: Kita bisa membiarkannya handleErrorOne, karena hanya akan dipanggil jika stepOneditolak (ini adalah fungsi pertama dalam rantai, jadi kita tahu bahwa jika rantai ditolak pada titik ini, itu hanya bisa karena janji fungsi itu) .

Perubahan penting adalah bahwa penangan kesalahan untuk fungsi lain bukan bagian dari rantai perjanjian utama. Sebaliknya, setiap langkah memiliki "sub-rantai" -nya sendiri dengan onRejectedyang hanya dipanggil jika langkah tersebut ditolak (tetapi tidak dapat dicapai oleh rantai utama secara langsung).

Alasan ini berhasil adalah karena keduanya onFulfilleddan onRejectedmerupakan argumen opsional untuk thenmetode tersebut. Jika sebuah promise terpenuhi (yaitu diselesaikan) dan thenrantai berikutnya tidak memiliki onFulfilledpenangan, rantai akan berlanjut sampai ada penangan dengan penangan tersebut.

Ini berarti dua baris berikut ini setara:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

Tetapi baris berikut tidak sama dengan dua di atas:

stepOne().then(stepTwo).then(null, handleErrorOne)

Pustaka janji Angular $qdidasarkan pada pustaka kriskowal Q(yang memiliki API yang lebih kaya, tetapi berisi semua yang dapat Anda temukan di dalamnya $q). Dokumen API Q di GitHub terbukti berguna. Q mengimplementasikan spesifikasi Promises / A + , yang menjelaskan secara mendetail tentang cara thendan perilaku penyelesaian promise berfungsi dengan tepat.

EDIT:

Juga perlu diingat bahwa jika Anda ingin keluar dari rantai di penangan kesalahan Anda, itu perlu mengembalikan janji yang ditolak atau melempar Kesalahan (yang akan tertangkap dan dibungkus dalam janji yang ditolak secara otomatis). Jika Anda tidak mengembalikan suatu janji, thenbungkus nilai pengembaliannya dalam sebuah janji keputusan untuk Anda.

Ini berarti bahwa jika Anda tidak mengembalikan apa pun, Anda secara efektif mengembalikan janji yang telah ditetapkan untuk nilainya undefined.

Alan Plum
sumber
140
Bagian ini emas: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Terima kasih @pluma
Valerio
8
Ini memang benar. Saya mengeditnya untuk membuatnya lebih berani
Cyril CHAPON
apakah menolak keluar dari fungsi saat ini? misalnya menyelesaikan tidak akan dipanggil jika menolak dipanggil 1 `jika (buruk) {menolak (status); } menyelesaikan (hasil); `
SuperUberDuper
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Apakah ini benar-benar setara? Saya pikir jika terjadi penolakan di stepOnebaris kedua kode akan dieksekusi stepTwotetapi yang pertama hanya akan mengeksekusi handleErrorOnedan berhenti. Atau apakah saya melewatkan sesuatu?
JeFf
5
Tidak benar-benar memberikan solusi yang jelas untuk pertanyaan yang diajukan, namun penjelasan yang baik
Yerken
59

Agak terlambat ke pesta tetapi solusi sederhana ini berhasil untuk saya:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

Hal ini memungkinkan Anda untuk memecahkan keluar dari rantai.

Vinnyq12
sumber
2
Membantu saya tetapi FYI, Anda dapat mengembalikannya saat itu untuk keluar di tangkapan seperti:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder
1
@CraigvanTonder Anda bisa memberikan janji dan itu akan bekerja sama seperti kode Anda:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia
1
Ini satu-satunya jawaban yang benar. Jika tidak, langkah 3 akan tetap dijalankan meskipun langkah 1 memiliki kesalahan.
wdetac
1
Hanya untuk memperjelas, jika terjadi kesalahan di stepOne (), maka chainError akan dipanggil, bukan? Jika ini diinginkan. Saya memiliki cuplikan yang melakukan ini, tidak yakin apakah saya salah paham - runkit.com/embed/9q2q3rjxdar9
user320550
10

Yang Anda butuhkan adalah .then()rantai berulang dengan kasing khusus untuk memulai dan kasing khusus untuk diselesaikan.

Keahliannya adalah untuk mendapatkan nomor langkah dari kasus kegagalan untuk melewati ke penanganan kesalahan akhir.

  • Mulai: panggil step(1)tanpa syarat.
  • Pola berulang: rantai a .then()dengan callback berikut:
    • sukses: panggil langkah (n + 1)
    • kegagalan: membuang nilai yang sebelumnya ditangguhkan ditolak atau memunculkan kembali kesalahan.
  • Selesai: rantai a .then()tanpa penangan sukses dan penangan kesalahan akhir.

Anda dapat menulis semuanya dengan tangan tetapi lebih mudah untuk mendemonstrasikan pola dengan fungsi yang diberi nama dan digeneralisasikan:

function nextStep(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(null, finalError);});

lihat demo

Perhatikan bagaimana masuk step(), penangguhan ditolak atau diselesaikan dengan n, sehingga membuat nilai tersebut tersedia untuk callback berikutnya .then()dalam rantai. Setelah stepErrordipanggil, kesalahan berulang kali ditampilkan kembali hingga ditangani oleh finalError.

Beetroot-Beetroot
sumber
Jawaban informatif jadi layak disimpan, tapi bukan itu masalah yang saya hadapi. Saya menyebutkan solusi ini di posting saya dan itu bukan yang saya cari. Lihat demo di bagian atas posting saya.
m59
1
m59, ini adalah jawaban atas pertanyaan yang diajukan, "bagaimana cara menulis janji sehingga saya dapat memanggil fungsi pada penolakan, tanpa memanggil semua fungsi dalam rantai kesalahan?" dan judul pertanyaan, "Putuskan rantai janji dan panggil fungsi berdasarkan langkah dalam rantai di mana ia diputuskan (ditolak)"
Beetroot-Beetroot
Benar, seperti yang saya katakan, ini informatif dan saya bahkan menyertakan solusi ini di posting saya (dengan kurang detail). Pendekatan ini dimaksudkan untuk memperbaiki berbagai hal agar rantai dapat berlanjut. Meskipun dapat mencapai apa yang saya cari, itu tidak sealami pendekatan dalam jawaban yang diterima. Dengan kata lain, jika Anda ingin melakukan apa yang diungkapkan oleh judul dan pertanyaan yang diajukan, lakukan pendekatan pluma.
m59
7

Saat menolak, Anda harus meneruskan kesalahan penolakan, kemudian membungkus penanganan kesalahan langkah dalam fungsi yang memeriksa apakah penolakan harus diproses atau "dicabut kembali" hingga akhir rantai:

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

Apa yang Anda lihat di konsol:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

Berikut beberapa kode yang berfungsi https://jsfiddle.net/8hzg5s7m/3/

Jika Anda memiliki penanganan khusus untuk setiap langkah, pembungkus Anda bisa seperti ini:

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

lalu rantaimu

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});
redben
sumber
2

Jika saya mengerti dengan benar, Anda hanya ingin menampilkan kesalahan untuk langkah yang gagal, bukan?

Itu harus sesederhana mengubah kasus kegagalan janji pertama menjadi ini:

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

Dengan kembali $q.reject()pada kasus kegagalan langkah pertama, Anda menolak janji itu, yang menyebabkan errorCallback dipanggil di langkah kedua then(...).

Zajn
sumber
Apa di dunia ... itulah yang saya lakukan! Lihat di posting saya bahwa saya memang mencobanya, tetapi rantai akan kembali masuk dan lari step(2). Sekarang saya coba lagi itu tidak terjadi. Aku begitu bingung.
m59
1
Saya melihat bahwa Anda menyebutkan itu. Itu aneh. Fungsi yang berisi return step(2);itu hanya boleh dipanggil jika step(1)penyelesaian berhasil.
Zajn
Gores itu - ini pasti terjadi. Seperti yang saya katakan di posting saya, jika Anda tidak menggunakan return $q.reject(), rantai akan terus berjalan. Dalam hal ini return responsemengacaukannya. Lihat ini: jsbin.com/EpaZIsIp/6/edit
m59
Hmm baiklah. Tampaknya berfungsi di jsbin yang Anda posting ketika saya mengubahnya, tetapi saya pasti melewatkan sesuatu.
Zajn
Ya, saya pasti melihat itu tidak berfungsi sekarang. Kembali ke papan gambar untukku!
Zajn
2
var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

Atau otomatis untuk sejumlah langkah:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit

Esailija
sumber
Tetapi jika saya akan menelepon deferred.reject(n)maka saya mendapatkan peringatan bahwa janji ditolak dengan objek
nonError
2

Coba ro gunakan ini seperti libs:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});
Leonid
sumber
2

Jika Anda ingin menyelesaikan masalah ini menggunakan async / await:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()
luispa
sumber
1

Lampirkan penangan kesalahan sebagai elemen rantai terpisah langsung ke pelaksanaan langkah-langkah:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

atau menggunakan catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

Catatan: Ini pada dasarnya adalah pola yang sama seperti yang disarankan oleh pluma dalam jawabannya tetapi menggunakan penamaan OP.

Ignitor
sumber
1

Promise.prototype.catch()Contoh yang ditemukan di MDN di bawah ini sangat membantu.

(Jawaban yang diterima menyebutkan then(null, onErrorHandler)yang pada dasarnya sama dengan catch(onErrorHandler).)

Menggunakan dan merangkai metode tangkapan

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

Gotchas saat melempar kesalahan

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

Jika sudah teratasi

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
toraritte
sumber
1

Solusi terbaik adalah melakukan refactor pada rantai janji Anda untuk menggunakan ES6 menunggu. Kemudian Anda bisa kembali dari fungsi untuk melewati perilaku lainnya.

Saya telah membenturkan kepala saya terhadap pola ini selama lebih dari setahun dan menggunakan await's adalah surga.

Pete Alvin
sumber
Saat menggunakan IE murni, async / await tidak didukung.
ndee
0

Jika suatu saat Anda kembali, Promise.reject('something')Anda akan dilemparkan ke dalam blok tangkapan pada janji itu.

promiseOne
  .then((result) => {
    if (!result) {
      return Promise.reject('No result');
    }
    return;
  })
  .catch((err) => {
    console.log(err);
  });

Jika janji pertama tidak memberikan hasil apa pun, Anda hanya akan mendapatkan 'Tidak ada hasil' di konsol.

Dimitar Gospodinov
sumber
-1

Gunakan Modul SequentialPromise

Niat

Sediakan modul yang tanggung jawabnya untuk menjalankan permintaan secara berurutan, sambil melacak indeks saat ini dari setiap operasi secara ordinal. Tentukan operasi dalam Pola Perintah untuk fleksibilitas.

Peserta

  • Konteks : Objek yang metode anggotanya melakukan operasi.
  • SequentialPromise : Mendefinisikan executemetode untuk merangkai & melacak setiap operasi. SequentialPromise mengembalikan Rantai-Janji dari semua operasi yang dilakukan.
  • Invoker : Membuat instance SequentialPromise, memberinya konteks & tindakan, dan memanggil executemetodenya sambil meneruskan daftar opsi ordinal untuk setiap operasi.

Konsekuensi

Gunakan SequentialPromise ketika perilaku ordinal dari resolusi Promise diperlukan. SequentialPromise akan melacak indeks yang Janji ditolaknya.

Penerapan

clear();

var http = {
    get(url) {
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log(`REQUEST`, url, delay);
        xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );

        function exe(resolve, reject) {
            var action = { 'true': reject, 'false': resolve }[ even ];
            setTimeout( () => action({ url, delay }), (1000 * delay) );
        }

        return xhr;
    }
};

var SequentialPromise = new (function SequentialPromise() {
    var PRIVATE = this;

    return class SequentialPromise {

        constructor(context, action) {
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        }

        log() {}

        execute(url, ...more) {
            var { context, action, requests } = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        }

    };
})();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn(`EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}`) );

// console.log('>', chain, promises);

Inti

SequentialPromise

Cody
sumber