microworlds
microworlds

Reputation: 67

Modify and return an object inside an async function

I want to modify an object (userTracker) inside an asynchronous js code. After running a function (fetchUsers), I want to have an updated fields on the said object. But I keep on getting undefined after modifying the object in the function. How can I achieve that?

My code:

let userTracker = {
    counter : 0,
    data : []
}

function fetchUsers(userTracker) {
    let filter  = {"some" : "conditions"}
    User.find(filter).exec() // I am deliberately not calling the `limit()` function. I have a goal to achieve
    .then( (users ) => {
        return users.forEach((user) => {

            if (userTracker.counter < 20) {
                userTracker.data.push(user)
                userTracker.counter += 1
            } else {
                return 
            }
        })
    })
    .catch(err => {
        console.log(err)
    })
}

fetchUsers(userTracker)
console.log(userTracker) // It logs `{}`. Accessing any field on the object shows `undefined`

How do I have access to the populated userTracker object after running the fetchUsers function? I want to end up with something like this

What I want to achieve:

userTracker = {
    counter : 8,
    data : [
        user1, user2, user3, ...
    ]
}

I suck at async programming, any help on how to achieve the desired output above is greatly appreciated.

Upvotes: 2

Views: 2692

Answers (2)

Julian
Julian

Reputation: 4366

The answer by @shivashriganesh-mahato is correct, but I'd like to explain why your original code doesn't work.

Whenever you pass a callback to a function, that function may invoke it in one of two ways: (1) sync or (2) async. Sync is easy to understand; it means that the callback is executed during the execution of the function to which you passed the callback.

Async is very different. Execution of the callback is postponed until all the sync code that is currently executing is done. That includes the function to which you passed the callback, the function that includes the line in which you call that function, and so on. The callback is postponed at least until the entire current call stack has completed executing, and possibly still longer after that. For example, when you send a network request and you pass a callback to handle the response, the callback is not only postponed until after the call stack clears, but also until after the response arrives. It may take an eternity from a computer point of view (i.e., several milliseconds).

Async function invocations are not pushed on top of the call stack. Instead, they are put at the end of a queue of future function invocations, each of which will start a new call stack of its own.

How do you catch a value change that happens only after the current call stack unwinds? By queuing another async function invocation after it. The following example code illustrates the difference with simple timeouts:

// sync
let count = 0;
function increment() { ++count; }

function invokesCallbackSync(callback) {
    callback();
}

invokesCallbackSync(increment);

console.log(count); // 1

// async
count = 0;

function invokesCallbackAsync(callback) {
    setTimeout(callback);
}

invokesCallbackAsync(increment);

console.log(count); // 0

setTimeout(() => console.log(count)); // 1 (eventually)

The .then method of a promise invokes its callback async, too. The nice thing about then is that it always returns a new promise, so you can queue another async callback after the completion of the first promise, and so on. So the minimal change to make your code work is as follows:

let userTracker = {
    counter : 0,
    data : []
}

function fetchUsers(userTracker) {
    let filter  = {"some" : "conditions"}
    return User.find(filter).exec()
    .then( (users ) => {
        return users.forEach((user) => {

            if (userTracker.counter < 20) {
                userTracker.data.push(user)
                userTracker.counter += 1
            } else {
                return 
            }
        })
    })
    .catch(err => {
        console.log(err)
    })
}

fetchUsers(userTracker).then(() => console.log(userTracker))

Upvotes: 1

Shivashriganesh Mahato
Shivashriganesh Mahato

Reputation: 572

Per the specification, the find function returns a Promise which you can await after wrapping everything you want to execute asynchronously in async functions, like so:

let userTracker = {
    counter : 0,
    data : []
}

async function fetchUsers(userTracker) {
    let filter = {"some" : "conditions"}
    let users = await User.find(filter).exec()

    users.forEach((user) => {
        if (userTracker.counter < 20) {
            userTracker.data.push(user)
            userTracker.counter += 1
        } else {
            return 
        }
    })
}

async function container() {
    await fetchUsers(userTracker)
    console.log(userTracker)
}

container()

Also quick suggestion: forEach isn't the best iteration method in this case. Since you want to stop adding users after 20, it would make sense to just exit the fetchUsers function after 20. For example, if there are 10000 users it would have to unnecessarily go through all of them and run the comparison, certainly a bottleneck. And there's no way to exit the parent function from inside a forEach. That said, it's better to use a traditional loop and call return after the counter reaches 20 which will exit fetchUsers and stop iterating users.

Upvotes: 2

Related Questions