164

I have an i18n service in my application which contains the following code:

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

The idea is that the user would call ensureLocaleIsLoaded and wait for the promise to be resolved. But given that the purpose of the function is to only ensure that the locale is loaded, it would be perfectly fine for the user to invoke it several times.

I'm currently just storing a single promise and resolve it if the user calls the function again after the locale has been successfully retrieved from the server.

From what I can tell, this is working as intended, but I'm wondering if this is a proper approach.

Oliver Salzburg
  • 21,652
  • 20
  • 93
  • 138

8 Answers8

204

As I understand promises at present, this should be 100% fine. The only thing to understand is that once resolved (or rejected), that is it for a defered object - it is done.

If you call then(...) on its promise again, you immediately get the (first) resolved/rejected result.

Additional calls to resolve() will not have any effect.

Below is an executable snippet that covers those use cases:

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));
BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
demaniak
  • 3,716
  • 1
  • 29
  • 34
  • 37
    Here's a JSBin illustrating that all of above is actually true: http://jsbin.com/gemepay/3/edit?js,console Only the first resolve is ever used. – konrad Feb 09 '17 at 08:47
  • 9
    Has anyone found any official documentation about this? It's generally inadvisable to rely on undocumented behavior even if it works right now. – 3ocene Sep 05 '18 at 18:26
  • 3
    https://www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve - I have to date not found anything that states that it is inherently UNSAFE. If your handler does something that really should only be done ONCE, I would have it check and update some state before performing the action again. But I would also like some official MDN entry or spec doc to get absolute clarity. – demaniak Sep 06 '18 at 07:48
  • I can not see anything "troubling" in the PromiseA+ page. See https://promisesaplus.com/ – demaniak Sep 06 '18 at 08:06
  • 1
    "you should immediately get the", not immediately/synchronously...it will be in the next tick of the event loop -(from the microtask queue). – Alexander Mills Oct 03 '18 at 04:09
  • 8
    @demaniak This question is about [Promises/A+](https://promisesaplus.com/), not ES6 promises. But to answer your question, the part of the ES6 spec about extraneous resolve/reject being safe is [here](https://www.ecma-international.org/ecma-262/6.0/#sec-promise-resolve-functions). – Trevor Robinson Oct 23 '18 at 20:58
1

I faced the same thing a while ago, indeed a promise can be only resolved once, another tries will do nothing (no error, no warning, no then invocation).

I decided to work it around like this:

getUsers(users => showThem(users));

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

just pass your function as a callback and invoke it as many times you wish! Hope that makes sense.

DamianoPantani
  • 1,168
  • 2
  • 13
  • 23
  • 1
    I think this is wrong. You could simply return the promise from `getUsers` and then invoke `.then()` on that promise as many times as you want. There is no need to pass a callback. In my opinion one of the advantages of promises is that you don't need to specify the callback up front. – John Henckel Oct 13 '19 at 16:32
  • 1
    @JohnHenckel The idea is to resolve the promise multiple times, i.e return data multiple times, not have multiple `.then` statements. For what it's worth, I think the only way to return data multiple times to the calling context is to use callbacks and not promises, as promises weren't built to work in that manner. – SamAko Apr 29 '20 at 14:15
  • 2
    You can simplify `.then(users => callback(users))` to `.then(callback)`. – ErikE Jan 19 '23 at 19:13
  • @ErikE How does this work? I have promise being passed from another sowftware component that knows where anad how it is pulling data from a remote plcace. The component that receives this promise needs to be able to "refresh" the data by requesting mode data. I'm thinking of cloning a backup each time I make the call. – TheRealChx101 Aug 02 '23 at 18:09
  • @TheRealChx101 Please ask a new question. It needs to have a lot more detail about what you want to accomplish. I really can't tell from your description what it is you want to do! – ErikE Aug 04 '23 at 02:29
  • @ErikE It's alright now. ChatGPT solved it. This site is going down anyway xD – TheRealChx101 Aug 04 '23 at 21:09
  • @TheRealChx101 Good luck with ChatGPT. Hope it works out for you how you expect. – ErikE Aug 07 '23 at 16:53
1

There s no clear way to resolve promises multiple times because since it's resolved it's done. The better approach here is to use observer-observable pattern for example i wrote following code that observes socket client event. You can extend this code to met your need

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

If you need to change the return value of promise, simply return new value in then and chain next then/catch on it

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);
});
Buksy
  • 11,571
  • 9
  • 62
  • 69
0

You can write tests to confirm the behavior.

By running the following test you can conclude that

The resolve()/reject() call never throw error.

Once settled (rejected), the resolved value (rejected error) will be preserved regardless of following resolve() or reject() calls.

You can also check my blog post for details.

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

})
Community
  • 1
  • 1
Sang
  • 4,049
  • 3
  • 37
  • 47
-2

What you should do is put an ng-if on your main ng-outlet and show a loading spinner instead. Once your locale is loaded the you show the outlet and let the component hierarchy render. This way all of your application can assume that the locale is loaded and no checks are necessary.

Adrian Brand
  • 20,384
  • 4
  • 39
  • 60
-2

see github gist: reuse_promise.js

/*
reuse a promise for multiple resolve()s since promises only resolve once and then never again
*/

import React, { useEffect, useState } from 'react'

export default () => {
    
    const [somePromise, setSomePromise] = useState(promiseCreator())
        
    useEffect(() => {
        
        somePromise.then(data => {
            
            // do things here
            
            setSomePromise(promiseCreator())
        })
        
    }, [somePromise])
}

const promiseCreator = () => {
    return new Promise((resolve, reject) => {
        // do things
        resolve(/*data*/)
    })
}
ddaaggeett
  • 149
  • 2
  • 10
-3

No. It is not safe to resolve/reject promise multiple times. It is basically a bug, that is hard to catch, becasue it can be not always reproducible.

There is pattern that can be used to trace such issues in debug time. Great lecture on this topic: Ruben Bridgewater — Error handling: doing it right! (the part related to the question is around 40 min)

bFunc
  • 1,370
  • 1
  • 12
  • 22