Разорвать цепочку обещаний и вызвать функцию на основе шага в цепочке, где она нарушена (отклонена)


обновление:

, чтобы помочь будущим зрителям этого поста, я создал эта демонстрация ответа pluma.

вопрос:

моя цель кажется довольно простой.

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

проблема здесь в том, что если я потерплю неудачу на шаге 1, оба stepError(1) и stepError(2) уволен. Если я этого не сделаю return $q.reject затем stepError(2) не будет уволен, но step(2) будет, как я понимаю. Я достиг всего, кроме того, что я пытаюсь делать.

Как написать обещания, чтобы я мог вызвать функцию при отклонении, не вызывая все функции в цепочке ошибок? Или есть другой способ сделать это?

вот демо так у тебя есть с чем работать.

обновление:

я вид ее решили. Здесь я ловлю ошибку в конце цепочки и передаю данные в reject(data) так что я буду знать, что проблема в функции ошибки. Это на самом деле не соответствует моим требованиям, потому что я не хочу зависеть от данных. Это было бы хромым, но в моем случае было бы чище передать обратный вызов ошибки функции, а не зависеть от возвращенных данных, чтобы определить, что делать.

демо здесь (Нажать).

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

9 ответов:

причина, по которой ваш код не работает так, как ожидалось, заключается в том, что он на самом деле делает что-то другое, чем вы думаете.

допустим, у вас есть что-то вроде следующего:

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

чтобы лучше понять, что происходит, давайте представим, что это синхронный код try/catch блоки:

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 onRejected обработчик (второй аргумент then) по существу является механизмом исправления ошибок (например,catch блок). Если ошибка выдается в handleErrorOne, он будет пойман следующим блоком catch (catch(e2)), и так далее.

это явно не то, что вы имели в виду.

предположим, что мы хотим, чтобы вся цепочка разрешения потерпела неудачу независимо от того, что идет не так:

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

Примечание: мы можем оставить handleErrorOne где он находится, потому что он будет вызван только если stepOne отклоняет (это первая функция в цепочке, поэтому мы знаем, что если цепь отклонена в этот момент, она может быть только из-за обещания этой функции).

важным изменением является то, что обработчики ошибок для других функций не являются частью основной цепочки обещаний. Вместо этого каждый шаг имеет свою собственную "подцепку" с onRejected это вызывается только в том случае, если шаг был отклонен (но не может быть достигнут непосредственно основной цепью).

причина, по которой это работает, заключается в том, что оба onFulfilled и onRejected являются необязательными аргументами для then метод. Если обещание выполнено (т. е. разрешено) и следующий then в сети нет onFulfilled обработчик, цепочка будет продолжаться до тех пор, пока не появится один с таким обработчиком.

это означает, что следующие две строки эквивалентны:

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

но следующая строка не эквивалентно двум выше:

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

библиотека обещаний Angular $q основан на kriskowal's Q библиотека (которая имеет более богатый API, но содержит все, что вы можете найти в $q). Вопрос по API docs на GitHub может оказаться полезным. Вопрос реализует обещания/A + spec, который подробно рассказывает о том, как then и поведение разрешения обещания работает точно.

EDIT:

также имейте в виду, что если вы хотите вырваться из цепочки в своем обработчике ошибок, он должен вернуть отклоненное обещание или бросить ошибку (которая будет поймана и обернута в отклоненное обещание автоматически). Если ты не вернешь обещание,then обертывает возвращаемое значение в обещание разрешения для вас.

это означает, что если вы ничего не возвращаете, вы фактически возвращаете разрешенное обещание для значения undefined.

немного опоздала на вечеринку, но это простое решение сработало для меня:

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

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

Это позволяет перерыв из цепи.

то, что вам нужно, это повторение .then() цепь со специальным случаем для начала и специальным случаем для завершения.

ловкость заключается в том, чтобы получить номер шага случая сбоя для пульсации до конечного обработчика ошибок.

  • Start: call step(1) безоговорочно.
  • повторяющийся шаблон: цепь a .then() со следующими ответами:
    • успех: вызов шаг (n+1)
    • failure: бросьте значение, с которым предыдущий отложенный был отклонен или переосмыслен ошибка.
  • отделка: цепь a .then() без обработчика успеха и окончательного обработчика ошибок.

вы можете написать все это от руки, но легче продемонстрировать шаблон с именованными, обобщенными функциями:

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

посмотреть демо

обратите внимание, как в step(), отложенное отклоняется или разрешается с помощью n, таким образом, делая это значение доступно обратные вызовы в следующем .then() в цепи. Один раз stepError вызывается, ошибка повторно перестраивается, пока она не будет обработана finalError.

при отклонении вы должны передать ошибку отклонения, а затем обернуть обработчики ошибок шага в функцию, которая проверяет, следует ли обрабатывать отклонение или" перестраивать " до конца цепочки:

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

что вы увидите в консоли :

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

вот рабочий код https://jsfiddle.net/8hzg5s7m/3/

если у вас есть определенная обработка для каждого шага, ваша обертка может быть чем-то вроде:

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

затем ваша цепь

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

Если я правильно понимаю, вы хотите, чтобы только ошибка для неудачного шага, чтобы показать, не так ли?

Это должно быть так же просто, как изменить случай отказа Первого обещания на это:

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

вернуться $q.reject() в случае неудачи первого шага вы отвергаете это обещание, что приводит к вызову errorCallback во 2-м then(...).

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

или автоматизировано для любого количества шагов:

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

прикрепите обработчики ошибок в виде отдельных элементов цепочки непосредственно к выполнению шагов:

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

или через 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(); });
});

Примечание: это в основном тот же шаблон, что и плума предлагает в своем ответе но с использованием именования OP.

нашел Promise.prototype.catch() примеры на MDN ниже очень полезная.

(принятый ответ упоминает then(null, onErrorHandler) что в основном то же самое, что catch(onErrorHandler).)

использование и цепочка метода catch

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 при выбрасывании ошибок

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

если он разрешен

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

самое лучшее решение-это оптимизация цепочки обещаем использовать ЕС6 ждут все. Затем вы можете просто вернуть из функции, чтобы пропустить оставшуюся часть поведения.

Я уже больше года бьюсь головой об эту модель и использую await's is heaven.