0

I have a Javascript function that looks like this in a personal project:

function buildSettings(settings_file_names){

    return new Promise(function(resolve, reject){

        let settings    = {};

        settings.files  = {};
        
        settings.getFile = function(file_name){
            return this.files[file_name];
        }
        
        settings_file_names.forEach((file_name) => {
            settings.files[file_name] = getLocalFile(file_name);
            //getLocalFile() returns a promise.
        });

        Promise.all(Object.values(settings.files)).then(() => {
            for (let filename in settings.files){
                if (filename.includes(".json")){
                    settings.files[filename].then((filedata) => {
                        if (filedata){
                            console.log("Parsing into JSON!");
                            settings.file[filename] = JSON.parse(filedata);
                        }else{
                            settings.file[filename] = null;
                        }
                    });
                }
            }

            console.log("About to resolve!");
            resolve(settings);
        });



    });

}

And the console output looks like this:

utility_functions.js:148 About to resolve!
utility_functions.js:135 Parsing into JSON! (x8)

However, I need to wait to extract the values of those promises and parse them into JSON before resolving the overarching promise. I need to wait for what happens in the then()s. Is there a way to do this? Or am I facing this issue because I'm doing something wrong in a broader sense?

I expected then() to give me the value of a Promise synchronously if I already waited for that Promise to be fulfilled (as I think I did with Promise.all()). In reality then() seems to behave asynchronously even if the Promise is fulfilled.

Thank you in advance for any insight!

Shen
  • 13
  • 3
  • Why you call then, you have everyting passed to the .then function, `Promise.all(Object.values(settings.files)).then((results) => {` – Lk77 Aug 10 '23 at 15:08
  • 1
    *"In reality then() seems to behave asynchronously even if the Promise is fulfilled."* That's exactly right. It is guaranteed that the callback passed to `.then` does not execute in the current tick, so that the behavior is consistent whether the promise is already resolved or not. – Felix Kling Aug 10 '23 at 15:14
  • @Lk77 Good eye! You're right, I absolutely don't need that second then(). However, if I just get filedata from the first then(), I won't know what filename it's associated with. Is there a way I can still associate the two, the build that { filename1: filedata1, filename2: filedata2 ... } object? – Shen Aug 10 '23 at 15:28
  • @FelixKling This is good to know, thank you! I wish there was some way to synchronously extract the value of a fulfilled Promise, that'd solve this problem for me. But there might be a more elegant solution I'm not seeing too. – Shen Aug 10 '23 at 15:30
  • Unrelated, but why do you call `getLocalFile` on files that don't have the `.json` extension, only to later ignore the response for them? Looks like a waste... – trincot Aug 10 '23 at 15:34
  • @trincot There is another type of file (CSV) that gets retrieved, I just haven't implemented the handling yet. They'll be parsed into an array once I code that part. – Shen Aug 10 '23 at 15:43
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Aug 10 '23 at 22:33

1 Answers1

2

The callback that is given as argument to settings.files[filename].then executes asynchronously, even when the promise is already resolved (as is the case here). That is why things happen in the wrong order.

The solution is simple: Promise.all returns a promise that fulfils with a value: that is an array of all the promises' fulfilled values. So there is no more need to call the above quoted then.

Secondly, you don't even need to create a new "overarching" promise with new Promise: this is the Promise constructor antipattern. The idea is that Promise.all already returns a promise, and you work with that one.

Finally, I would not first create the settings data structure only to mutate it later. Delay that "structuring" until you have all resolved promises.

You can do it as follows:

function buildSettings(settings_file_names) {
    const promises = settings_file_names.map(getLocalFile);
    
    // Return the promise you get instead of creating a new one
    return Promise.all(promises).then((responses) => { // Use the argument!
        const pairs = settings_file_names.map((file_name) => {
            const filedata = responses.shift(); // Get the response!
            if (file_name.endsWith(".json")) {
                console.log("Parsing into JSON!");
                return [file_name, JSON.parse(filedata)];
            } else { // Other parsers like CSV...
                return [file_name, null];
            }
        });
        console.log("About to resolve!");
        // Only construct the settings data structure when finished.
        // And return it, so it becomes the overal resolved value
        return {
            files: Object.fromEntries(pairs),
            getFile(file_name) {
                return this.files[file_name];
            }
        };
    });
}
trincot
  • 317,000
  • 35
  • 244
  • 286
  • 1
    Just did it this way and it works perfectly. There's so much in here I didn't realize! Like that if a `functionA` returns a Promise with a `.then()` attached, calling `functionA.then()` continues the chain of that first `.then()`. Thanks for your help with this, @trincot :] – Shen Aug 10 '23 at 16:35