Trung Tran
Trung Tran

Reputation: 13741

How to assign object values using asynchronous functions in javascript

I am building a javascript object where some of the values are defined by asynchronous functions. My problem is the object gets defined faster than the asynchronous functions can return values:

    const array = ['a', 'b', 'c', 'd']
    const myArrayObj = [];

    function returnKey1 () {
        // make async call then store it in the return key1val:
        return key1val
    }

    function returnKey2 () {
        // make async call then store it in the return key2val:
        return key2val
    }

    function returnKey3 () {
        // make async call then store it in the return key3val:
        return key3val
    }

    _.forEach( array, function ( arr ) {
        myArrayObj.push({
            key1: returnKey1(), // returns undefined
            key2: returnKey2(), // returns undefined
            key3: returnKey3(), // returns undefined
      });

    });

Does anyone know the correct way I should be doing this? Thanks in advance!

Upvotes: 0

Views: 6480

Answers (3)

nem035
nem035

Reputation: 35491

The nature of asynchronicity is that you must wait for an async process to finish if you want to access its end result.

In your case, you could achieve this using promises with not a lot of code.:

// promise that resolves after 2 seconds
const timeoutPromise = (str) => new Promise(resolve => setTimeout(() => resolve(str), 2000));

// functions that return promises that will eventually resolve to appropriate key values 

function returnKey1() {
  return timeoutPromise('key3');
}

function returnKey2() {
  return timeoutPromise('key2');
}

function returnKey3() {
  return timeoutPromise('key3');
}

// helper function that returns a promise which will resolve with all the keys when all key-returning promises resolve

function getKeys() {
  return Promise.all([
    returnKey1(),
    returnKey2(),
    returnKey3()
  ])
}

// usage
getKeys().then((keys) => {
  console.log(
    keys[0],
    keys[1],
    keys[2]
  );
});

The old-school approach would be to use callbacks instead of promises, which have larger browser support but are much cruder and primitive.

Note: With modern transpilers and/or promise libraries you can obtain wide browser support for promises as well.

// function that calls its callback after 2 seconds
const timeoutCallback = (cb, key) => setTimeout(() => cb(key), 2000);

// functions that eventually call their callbacks with appropriate key values 

function returnKey1(cb) {
  return timeoutCallback(cb, 'key1');
}

function returnKey2(cb) {
  return timeoutCallback(cb, 'key2');
}

function returnKey3(cb) {
  return timeoutCallback(cb, 'key3');
}

// helper function that calls its callback when all the keys are obtained

function getKeys(cb) {
  let keys = [undefined, undefined, undefined];
  let hasAllKeys = () => keys.every(key => typeof key === 'string');

  function makeReturnKeyCallback(idx) {
    return (key) => {
      keys[idx] = key;
      if (hasAllKeys()) {
        cb(keys);
      }
    };
  }

  returnKey1(makeReturnKeyCallback(0));
  returnKey2(makeReturnKeyCallback(1));
  returnKey3(makeReturnKeyCallback(2));
}

// usage
getKeys((keys) => {
  console.log(
    keys[0],
    keys[1],
    keys[2]
  );
});

Upvotes: 2

user663031
user663031

Reputation:

Whatever you do will end up using promises. You can use Promise.all directly, or if you are willing to spend a bit more time you could write your own version of Promise.all for objects; finally, you could do this using async functions, which are a way to write promise-based code in a way which looks sort of synchronous.

Using Promise.all:

// Just examples for testing purposes.
function returnKey1() { return promise(); }
function returnKey2() { return promise(); }
function returnKey3() { return promise(); }

// Make a promise returning a random value after random time.
function promise() {
  return new Promise(resolve => {
    const random = Math.random() * 1000;
    setTimeout(() => resolve(random), random);
  });
}

// Wait for all promises to finish, then construct result.
Promise.all([returnKey1(), returnKey2(), returnKey3()])
  .then(([key1, key2, key3]) => ({key1, key2, key3}))
  .then(console.log);

Writing your own version of Promise.all for objects

We could also create an analog to Promise.all for objects. Some libraries have such a routine. We will call it promiseAllKeys. We will pass it an object each of whose property values in a promise; it returns a promise for an object with all the values filled in. This saves us the trouble of converting an array coming back from Promise.all into the desired object.

function returnKey1() { return promise(); }
function returnKey2() { return promise(); }
function returnKey3() { return promise(); }

function promise() {
  return new Promise(resolve => {
    const random = Math.random() * 1000;
    setTimeout(() => resolve(random), random);
  });
}

// Convenience routine to construct an object from arrays of keys and values.
function objectFromKeysAndValues(keys, values) {
  const result = {};
  for (const i = 0; i < keys.length; i++) result[keys[i]] = values[i];
  return result;
}
 
function promiseAllKeys(promises) {
  return Promise.all(Object.values(promises))
    .then(values => objectFromKeysAndValues(Object.keys(promises), values));
}

promiseAllKeys({key1: returnKey1(), key2: returnKey2(), key3: returnKey3()})
  .then(console.log);
    

Using async/await

You could simplify your code using async functions. However, this code as written will wait until each promise completes before running the next one.

function returnKey1() { return promise(); }
function returnKey2() { return promise(); }
function returnKey3() { return promise(); }

function promise() {
  return new Promise(resolve => {
    const random = Math.random() * 1000;
    setTimeout(() => resolve(random), random);
  });
}

async function makeObject() {
  return {key1: await returnKey1(), key2: await returnKey2(), key3: await returnKey3()};
}

makeObject().then(console.log);

Upvotes: 1

MartinWebb
MartinWebb

Reputation: 2008

Simple but messy:

const array = ['a', 'b', 'c', 'd']
const myArrayObj = [];

function returnKey1 (cb) {
    // make async call then store it in the return key1val:
    cb ("key1val");
}

function returnKey2 (cb) {
    // make async call then store it in the return key2val:
   cb("key2val");
}

function returnKey3 (cb) {
    // make async call then store it in the return key3val:
    cb("key3val");
}

_.forEach( array, function ( arr ) {
    var o ={};

    returnKey1(function(key){
        o.key1=key;
      returnKey2(function(key){
        o.key2=key;
        returnKey3(function(key){
            o.key3=key
          myArrayObj.push(obj);
        })
      })
    })

  });

});

Upvotes: 0

Related Questions