Romper la cadena de la promesa y llamar a una función basada en el paso en la cadena donde está roto (rechazado)

135

Actualizar:

Para ayudar a los futuros espectadores de esta publicación, creé esta demostración de la respuesta de pluma .

Pregunta:

Mi objetivo parece bastante sencillo.

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

El problema aquí es que si fallo en el paso 1, ambos stepError(1)Y stepError(2)son despedidos. Si no lo hago return $q.rejectentonces stepError(2)no va a ser despedido, pero step(2)lo hará, que entiendo. He logrado todo excepto lo que estoy tratando de hacer.

¿Cómo escribo promesas para poder llamar a una función en caso de rechazo, sin llamar a todas las funciones en la cadena de error? ¿O hay otra forma de lograr esto?

Aquí hay una demostración en vivo para que tenga algo con lo que trabajar.

Actualizar:

De alguna manera lo he resuelto. Aquí, estoy captando el error al final de la cadena y pasando los datos para reject(data)que sepa qué problema manejar en la función de error. En realidad, esto no cumple con mis requisitos porque no quiero depender de los datos. Sería lamentable, pero en mi caso sería más limpio pasar una devolución de llamada de error a la función en lugar de depender de los datos devueltos para determinar qué hacer.

Demostración en vivo aquí (clic).

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
fuente
1
Hay una asincrónica JavaScript lib que podría ayudar si esto se vuelve más complicado
lúcuma
Promise.prototype.catch()ejemplos en MDN muestran solución para exactamente los mismos problemas.
toraritte

Respuestas:

199

La razón por la que su código no funciona como se espera es que en realidad está haciendo algo diferente de lo que cree que hace.

Digamos que tienes algo como lo siguiente:

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

Para comprender mejor lo que está sucediendo, imaginemos que este es un código síncrono con try/ catchblocks:

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

El onRejectedcontrolador (el segundo argumento de then) es esencialmente un mecanismo de corrección de errores (como un catchbloque). Si se genera un error handleErrorOne, será detectado por el siguiente bloque de captura ( catch(e2)), y así sucesivamente.

Obviamente, esto no es lo que pretendías.

Digamos que queremos que toda la cadena de resolución falle sin importar lo que salga mal:

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

Nota: Podemos dejar el lugar handleErrorOnedonde está, porque solo se invocará si se stepOnerechaza (es la primera función en la cadena, por lo que sabemos que si la cadena se rechaza en este punto, solo puede ser debido a la promesa de esa función) .

El cambio importante es que los manejadores de errores para las otras funciones no son parte de la cadena principal de promesa. En cambio, cada paso tiene su propia "subcadena" con una onRejectedque solo se llama si el paso fue rechazado (pero la cadena principal no puede alcanzarlo directamente).

La razón por la que esto funciona es que tanto onFulfilledy onRejectedson argumentos opcionales al thenmétodo. Si se cumple una promesa (es decir, se resuelve) y el siguiente thenen la cadena no tiene un onFulfilledcontrolador, la cadena continuará hasta que haya uno con dicho controlador.

Esto significa que las siguientes dos líneas son equivalentes:

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

Pero la siguiente línea no es equivalente a las dos anteriores:

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

La biblioteca de promesa de Angular $qse basa en la Qbiblioteca de kriskowal (que tiene una API más rica, pero contiene todo lo que puede encontrar $q). Los documentos API de Q en GitHub podrían resultar útiles. Q implementa la especificación Promises / A + , que detalla cómo theny cómo funciona exactamente el comportamiento de resolución de promesas.

EDITAR:

También tenga en cuenta que si desea salir de la cadena en su controlador de errores, debe devolver una promesa rechazada o lanzar un Error (que será atrapado y envuelto en una promesa rechazada automáticamente). Si no devuelve una promesa, thenenvuelve el valor de devolución en una promesa de resolución para usted.

Esto significa que si no devuelve nada, efectivamente está devolviendo una promesa resuelta por el valor undefined.

Alan ciruela
fuente
138
Esta parte es oro: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Gracias @pluma
Valerio
77
Esto es de hecho. Lo estoy editando para darle la negrita que merece
Cyril CHAPON
Cómo rechaza salir de la función actual? por ejemplo, resolver no se llamará si rechazar se llama primero `if (malo) {rechazar (estado); } resolver (resultados); `
SuperUberDuper
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` ¿Son realmente equivalentes? Creo que en caso de rechazo en stepOnela segunda línea de código se ejecutará, stepTwopero la primera solo se ejecutará handleErrorOney se detendrá. ¿O me estoy perdiendo algo?
JeFf
55
Realmente no proporciona una solución clara para la pregunta formulada, sin embargo, una buena explicación
Yerken
57

Un poco tarde para la fiesta, pero esta solución simple funcionó para mí:

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

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

Esto le permite romper fuera de la cadena.

Vinnyq12
fuente
1
Me ayudó, pero para su información, puede devolverlo en el entonces para romper la captura como:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder
1
@CraigvanTonder puede simplemente cumplir una promesa y funcionará igual que el código suyo:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia
1
Esta es la única respuesta correcta. De lo contrario, el paso 3 aún se ejecutará, incluso el paso 1 tiene un error.
wdetac
1
Solo para aclarar, si se produce un error en stepOne (), entonces se invoca tanto el chainError ¿no? Si esto es deseable. Tengo un fragmento que hace esto, no estoy seguro si mal entendido nada i - runkit.com/embed/9q2q3rjxdar9
user320550
10

Lo que necesita es una .then()cadena repetitiva con un estuche especial para comenzar y un estuche especial para terminar.

La habilidad es obtener el número de paso del caso de falla para pasar a un controlador de error final.

  • Inicio: llame step(1)incondicionalmente.
  • Patrón repetitivo: encadena a .then()con las siguientes devoluciones de llamada:
    • éxito: paso de llamada (n + 1)
    • fallo: arroje el valor con el que se rechazó la referencia anterior o vuelva a lanzar el error.
  • Finalizar: encadene un .then()controlador sin éxito y un controlador de error final.

Puede escribir todo a mano pero es más fácil demostrar el patrón con funciones generalizadas con nombre:

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

ver demo

Observe cómo step(), el diferido se rechaza o resuelve con n, haciendo que ese valor esté disponible para las devoluciones de llamada en el siguiente .then()en la cadena. Una vez que stepErrorse llama, el error se vuelve a lanzar repetidamente hasta que lo maneja finalError.

Remolacha-Remolacha
fuente
Respuesta informativa, así que vale la pena mantenerla, pero ese no es el problema que estoy enfrentando. Menciono esta solución en mi publicación y no es lo que estoy buscando. Vea la demostración en la parte superior de mi publicación.
m59
1
m59, esta es una respuesta a la pregunta formulada, "¿cómo escribo promesas para poder llamar a una función en caso de rechazo, sin llamar a todas las funciones en la cadena de error?" y el título de la pregunta, "Romper la cadena de promesa y llamar a una función basada en el paso en la cadena donde se rompe (rechazado)"
Remolacha-Remolacha
Correcto, como dije, es informativo e incluso incluí esta solución en mi publicación (con menos detalles). Este enfoque está destinado a arreglar cosas para que la cadena pueda continuar. Si bien puede lograr lo que estoy buscando, no es tan natural como el enfoque en la respuesta aceptada. En otras palabras, si desea hacer lo que se expresa en el título y la pregunta, adopte el enfoque de pluma.
m59
7

Al rechazar, debe pasar un error de rechazo, luego envolver los manejadores de errores de paso en una función que verifique si el rechazo debe procesarse o "volverse a lanzar" hasta el final de la cadena:

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

Lo que verías en la consola:

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

Aquí hay un código de trabajo https://jsfiddle.net/8hzg5s7m/3/

Si tiene un manejo específico para cada paso, su contenedor podría ser algo como:

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

entonces tu cadena

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
fuente
2

Si entiendo correctamente, solo desea que se muestre el error para el paso que falla, ¿verdad?

Eso debería ser tan simple como cambiar el caso de fracaso de la primera promesa a esto:

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

Al regresar $q.reject()en el caso de falla del primer paso, está rechazando esa promesa, lo que hace que se llame al errorCallback en el segundo then(...).

Zajn
fuente
¡Qué demonios ... eso es exactamente lo que hice! Vea en mi publicación que intenté eso, pero la cadena volvería a funcionar y funcionaría step(2). Ahora lo intenté de nuevo, no está sucediendo. Estoy tan confundida.
m59
1
Vi que mencionaste eso. Sin embargo, eso es extraño. Esa función que contiene return step(2);solo debería llamarse cuando se step(1)resuelve con éxito.
Zajn
Rasca eso, definitivamente está sucediendo. Como dije en mi publicación, si no la usas return $q.reject(), la cadena continuará. En este caso lo return responseestropeó. Vea esto: jsbin.com/EpaZIsIp/6/edit
m59
Hmm, ok. Parece que funciona en el jsbin que publicaste cuando cambié eso, pero debo haberme perdido algo.
Zajn
Sí, definitivamente veo que no funciona ahora. De vuelta a la mesa de dibujo para mí!
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

O automatizado para cualquier número de pasos:

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
fuente
Pero si voy a llamar deferred.reject(n)entonces yo estoy haciendo la advertencia que prometen rechazó con un objeto nonError
9ME
2

Intente usar esto como 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
fuente
2

Si desea resolver este problema usando 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
fuente
1

Adjunte controladores de errores como elementos de cadena separados directamente a la ejecución de los pasos:

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

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

Nota: Este es básicamente el mismo patrón que sugiere pluma en su respuesta, pero usando el nombre del OP.

Encendedor
fuente
1

Los Promise.prototype.catch()ejemplos encontrados en MDN a continuación son muy útiles.

(La respuesta aceptada menciona then(null, onErrorHandler)que es básicamente lo mismo que catch(onErrorHandler)).

Usar y encadenar el método de captura

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 al arrojar errores

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

Si se resuelve

//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
fuente
1

La mejor solución es refactorizar su cadena de promesa para usar ES6 en espera. Luego puede regresar de la función para omitir el resto del comportamiento.

He estado golpeándome la cabeza contra este patrón durante más de un año y usar el esperar es el cielo.

Pete Alvin
fuente
Cuando se usa IE async / await puro no es compatible.
ndee
0

Utilice un módulo de promesa secuencial

Intención

Proporcione un módulo cuya responsabilidad sea ejecutar las solicitudes de forma secuencial, mientras realiza el seguimiento del índice actual de cada operación de manera ordinal. Defina la operación en un Patrón de comando para mayor flexibilidad.

Participantes

  • Contexto : el objeto cuyo método miembro realiza una operación.
  • SequentialPromise : define un executemétodo para encadenar y rastrear cada operación. SequentialPromise devuelve una Promise-Chain de todas las operaciones realizadas.
  • Invoker : crea una instancia de SequentialPromise, proporcionándole contexto y acción, y llama a su executemétodo mientras pasa una lista ordinal de opciones para cada operación.

Consecuencias

Use SequentialPromise cuando se necesita un comportamiento ordinal de resolución Promise. SequentialPromise rastreará el índice para el que se rechazó una Promesa.

Implementación

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

Esencia

Promesa secuencial

Cody
fuente
0

Si en algún momento regresas Promise.reject('something'), serás arrojado a la trampa de la promesa.

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

Si la primera promesa no devuelve ningún resultado, solo obtendrá 'Sin resultado' en la consola.

Dimitar Gospodinov
fuente