¿Es seguro resolver una promesa varias veces?

7 minutos de lectura

Tengo un servicio i18n en mi aplicación que contiene el siguiente código:

var i18nService = function() {
  this.ensureLocaleIsLoaded = function() {
    if( !this.existingPromise ) {
      this.existingPromise = $q.defer();

      var deferred = this.existingPromise;
      var userLanguage = $( "body" ).data( "language" );
      this.userLanguage = userLanguage;

      console.log( "Loading locale '" + userLanguage + "' from server..." );
      $http( { method:"get", url:"/i18n/" + userLanguage, cache:true } ).success( function( translations ) {
        $rootScope.i18n = translations;
        deferred.resolve( $rootScope.i18n );
      } );
    }

    if( $rootScope.i18n ) {
      this.existingPromise.resolve( $rootScope.i18n );
    }

    return this.existingPromise.promise;
  };

La idea es que el usuario llame ensureLocaleIsLoaded y esperar a que se resuelva la promesa. Pero dado que el propósito de la función es solo asegurar que la configuración regional está cargada, estaría perfectamente bien que el usuario la invoque varias veces.

Actualmente solo estoy almacenando una sola promesa y la resuelvo si el usuario vuelve a llamar a la función después de que la configuración regional se haya recuperado con éxito del servidor.

Por lo que puedo decir, esto está funcionando según lo previsto, pero me pregunto si este es un enfoque adecuado.

  • Ver esta respuesta.

    – robertklep

    2 de diciembre de 2013 a las 12:48

  • Yo también lo he usado y funciona bien.

    – Chandermani

    2 de diciembre de 2013 a las 13:38

avatar de usuario
demaníaco

Según entiendo las promesas en la actualidad, esto debería estar 100% bien. Lo único que hay que entender es que una vez resuelto (o rechazado), eso es todo para un objeto diferido: está hecho.

si llamas then(...) en su promesa nuevamente, obtiene inmediatamente el (primer) resultado resuelto/rechazado.

Llamadas adicionales a resolve() no tendrá ningún efecto.

A continuación se muestra un fragmento ejecutable que cubre esos casos de uso:

var p = new Promise((resolve, reject) => {
  resolve(1);
  reject(2);
  resolve(3);
});

p.then(x => console.log('resolved to ' + x))
 .catch(x => console.log('never called ' + x));

p.then(x => console.log('one more ' + x));
p.then(x => console.log('two more ' + x));
p.then(x => console.log('three more ' + x));

  • Aquí hay un JSBin que ilustra que todo lo anterior es realmente cierto: jsbin.com/gemepay/3/edit?js,consola Solo se usa la primera resolución.

    – Konrad

    9 de febrero de 2017 a las 8:47

  • ¿Alguien ha encontrado alguna documentación oficial sobre esto? Por lo general, es desaconsejable confiar en un comportamiento no documentado, incluso si funciona en este momento.

    – 3oceno

    5 sep 2018 a las 18:26

  • ecma-international.org/ecma-262/6.0/#sec-promise.resolve – Hasta la fecha no he encontrado nada que indique que es inherentemente INSEGURO. Si su controlador hace algo que realmente solo debería hacerse UNA VEZ, haría que verificara y actualizara algún estado antes de realizar la acción nuevamente. Pero también me gustaría una entrada oficial de MDN o un documento de especificaciones para obtener una claridad absoluta.

    – demaniak

    6 de septiembre de 2018 a las 7:48

  • “debe obtener inmediatamente el”, no inmediatamente/sincrónicamente… estará en el siguiente tic del bucle de eventos (de la cola de microtareas).

    – Alejandro Mills

    3 de octubre de 2018 a las 4:09

  • @demaniak Esta pregunta es sobre Promesas/A+, no promesas de ES6. Pero para responder a su pregunta, la parte de la especificación ES6 sobre la seguridad de la resolución/rechazo extraño es aquí.

    – Trevor Robinson

    23 oct 2018 a las 20:58

Me enfrenté a lo mismo hace un tiempo, de hecho, una promesa solo se puede resolver una vez, otros intentos no harán nada (sin error, sin advertencia, sin then invocación).

Decidí trabajarlo así:

getUsers(users => showThem(users));

getUsers(callback){
    callback(getCachedUsers())
    api.getUsers().then(users => callback(users))
}

simplemente pase su función como una devolución de llamada e invóquela tantas veces como desee. Espero que tenga sentido.

  • Creo que esto está mal. Simplemente podría devolver la promesa de getUsers y luego invocar .then() en esa promesa tantas veces como quieras. No hay necesidad de pasar una devolución de llamada. En mi opinión, una de las ventajas de las promesas es que no es necesario especificar la devolución de llamada por adelantado.

    – John Henkel

    13 oct 2019 a las 16:32

  • @JohnHenckel La idea es resolver la promesa varias veces, es decir, devolver datos varias veces, no tener múltiples .then declaraciones. Por lo que vale, creo que la única forma de devolver datos varias veces al contexto de llamada es usar devoluciones de llamada y no promesas, ya que las promesas no se crearon para funcionar de esa manera.

    – SamAko

    29 de abril de 2020 a las 14:15

No hay una forma clara de resolver las promesas varias veces porque, una vez que se resolvió, se completó. El mejor enfoque aquí es usar un patrón observable por el observador, por ejemplo, escribí el siguiente código que observa el evento del cliente de socket. Puede extender este código para satisfacer su necesidad

const evokeObjectMethodWithArgs = (methodName, args) => (src) => src[methodName].apply(null, args);
    const hasMethodName = (name) => (target = {}) => typeof target[name] === 'function';
    const Observable = function (fn) {
        const subscribers = [];
        this.subscribe = subscribers.push.bind(subscribers);
        const observer = {
            next: (...args) => subscribers.filter(hasMethodName('next')).forEach(evokeObjectMethodWithArgs('next', args))
        };
        setTimeout(() => {
            try {
                fn(observer);
            } catch (e) {
                subscribers.filter(hasMethodName('error')).forEach(evokeObjectMethodWithArgs('error', e));
            }
        });

    };

    const fromEvent = (target, eventName) => new Observable((obs) => target.on(eventName, obs.next));

    fromEvent(client, 'document:save').subscribe({
        async next(document, docName) {
            await writeFilePromise(resolve(dataDir, `${docName}`), document);
            client.emit('document:save', document);
        }
    });

Si necesita cambiar el valor de retorno de la promesa, simplemente devuelva el nuevo valor en then y cadena siguiente then/catch en eso

var p1 = new Promise((resolve, reject) => { resolve(1) });
    
var p2 = p1.then(v => {
  console.log("First then, value is", v);
  return 2;
});
    
p2.then(v => {
  console.log("Second then, value is", v);
});

avatar de usuario
Transang

Puede escribir pruebas para confirmar el comportamiento.

Al ejecutar la siguiente prueba se puede concluir que

La llamada resolve()/reject() nunca arroja un error.

Una vez liquidado (rechazado), el valor resuelto (error rechazado) se conservará independientemente de las llamadas siguientes a resolve() o rechazó().

También puedes comprobar mi entrada de blog para detalles.

/* eslint-disable prefer-promise-reject-errors */
const flipPromise = require('flip-promise').default

describe('promise', () => {
    test('error catch with resolve', () => new Promise(async (rs, rj) => {
        const getPromise = () => new Promise(resolve => {
            try {
                resolve()
            } catch (err) {
                rj('error caught in unexpected location')
            }
        })
        try {
            await getPromise()
            throw new Error('error thrown out side')
        } catch (e) {
            rs('error caught in expected location')
        }
    }))
    test('error catch with reject', () => new Promise(async (rs, rj) => {
        const getPromise = () => new Promise((_resolve, reject) => {
            try {
                reject()
            } catch (err) {
                rj('error caught in unexpected location')
            }
        })
        try {
            await getPromise()
        } catch (e) {
            try {
                throw new Error('error thrown out side')
            } catch (e){
                rs('error caught in expected location')
            }
        }
    }))
    test('await multiple times resolved promise', async () => {
        const pr = Promise.resolve(1)
        expect(await pr).toBe(1)
        expect(await pr).toBe(1)
    })
    test('await multiple times rejected promise', async () => {
        const pr = Promise.reject(1)
        expect(await flipPromise(pr)).toBe(1)
        expect(await flipPromise(pr)).toBe(1)
    })
    test('resolve multiple times', async () => {
        const pr = new Promise(resolve => {
            resolve(1)
            resolve(2)
            resolve(3)
        })
        expect(await pr).toBe(1)
    })
    test('resolve then reject', async () => {
        const pr = new Promise((resolve, reject) => {
            resolve(1)
            resolve(2)
            resolve(3)
            reject(4)
        })
        expect(await pr).toBe(1)
    })
    test('reject multiple times', async () => {
        const pr = new Promise((_resolve, reject) => {
            reject(1)
            reject(2)
            reject(3)
        })
        expect(await flipPromise(pr)).toBe(1)
    })

    test('reject then resolve', async () => {
        const pr = new Promise((resolve, reject) => {
            reject(1)
            reject(2)
            reject(3)
            resolve(4)
        })
        expect(await flipPromise(pr)).toBe(1)
    })
test('constructor is not async', async () => {
    let val
    let val1
    const pr = new Promise(resolve => {
        val = 1
        setTimeout(() => {
            resolve()
            val1 = 2
        })
    })
    expect(val).toBe(1)
    expect(val1).toBeUndefined()
    await pr
    expect(val).toBe(1)
    expect(val1).toBe(2)
})

})

avatar de usuario
Adrián marca

Lo que debe hacer es poner un ng-if en su principal ng-outlet y mostrar una rueda de carga en su lugar. Una vez que su configuración regional está cargada, muestra la salida y deja que la jerarquía de componentes se represente. De esta manera, toda su aplicación puede asumir que la configuración regional está cargada y no es necesario realizar comprobaciones.

avatar de usuario
bFunc

No. No es seguro resolver/rechazar la promesa varias veces. Básicamente es un error, que es difícil de atrapar, porque no siempre puede ser reproducible.

Hay un patrón que se puede usar para rastrear tales problemas en el tiempo de depuración. Gran conferencia sobre este tema: Ruben Bridgewater — Manejo de errores: ¡haciéndolo bien! (la parte relacionada con la pregunta es de unos 40 min)

  • ¿Por qué no es seguro? Te equivocas.

    – Playa de Elliott

    26 abr a las 15:16


¿Ha sido útil esta solución?