Why Does Firebase Lose Reference outside the once() Function?

I'm using Firebase along with angularJS to fetch a list of users. I can read all the users from my database with the once() function, but I can't figure out why userList returns undefined below

.service('userService', [function() {
    this.getUsers = function() {
        var users;
        var userList;
        var ref = firebase.database().ref('/users/');
        ref.once('value').then(function(snapshot) {
            users = snapshot.val();
            for(var key in users) {
                users[key].id = key;
                // do some other stuff
            }
            console.log(users); // outputs all users
        }).then(function(){
            userList = users; 
            console.log(userList); // outputs all users
        },(function(error){
            alert('error:  ' + error);
        });
        console.log(userList); // outputs 'undefined'
    }
}]);

I wait to assign my userList variable until I'm done processing users, but no luck.

There is something important here that I am missing where Promises/callbacks are concerned and I cannot find it in the documentation; could someone help me understand my issue?


The problem is (as imjared says) that the data is read from Firebase asynchronously. So the code doesn't execute in the order that you think. It's easiest to see that by simplifying it with just a few log statements:

.service('userService', [function() {
    this.getUsers = function() {
        var ref = firebase.database().ref('/users/');
        console.log("before attaching listener");
        ref.once('value').then(function(snapshot) {
            console.log("got value");
        });
        console.log("after attaching listener");
    }
}]);

The output of this will be:

before attaching listener

after attaching listener

got value

Knowing the order in which this executes should explain perfectly why you cannot print the user list after you've just attached the listener. If not, I recommend also reading this great answer: How do I return the response from an asynchronous call?

Now for the solution: you will either need to use the user list in the callback or return a promise.

Use the user list in the callback

This is the oldest way to deal with asynchronous code: move all code that needs the user list into the callback.

    ref.once('value', function(snapshot) {
        users = snapshot.val();
        for(var key in users) {
            users[key].id = key;
        }
        console.log(users); // outputs all users
    })

You're reframing your code from "first load the user list, then print its contents" to "whenever the user list is loaded, print its contents". The difference in definition is minor, but suddenly you're perfectly equipped to deal with asynchronous loading.

You can also do the same with a promise, like you do in your code:

   ref.once('value').then(function(snapshot) {
        users = snapshot.val();
        for(var key in users) {
            users[key].id = key;
            // do some other stuff
        }
        console.log(users); // outputs all users
    });

But using a promise has one huge advantage over using the callback directly: you can return a promise.

Return a promise

Often you won't want to put all the code that needs users into the getUsers() function. In that case you can either pass a callback into getUsers() (which I won't show here, but it's very similar to the callback you can pass into once()) or you can return a promise from getUsers():

this.getUsers = function() {
    var ref = firebase.database().ref('/users/');
    return ref.once('value').then(function(snapshot) {
        users = snapshot.val();
        for(var key in users) {
            users[key].id = key;
            // do some other stuff
        }
        return(users);
    }).catch(function(error){
        alert('error:  ' + error);
    });
}

With this service, we can now call getUsers() and use the resulting promise to get at the users once they're loaded:

userService.getUsers().then(function(userList) {
    console.log(userList);
})

And with that you have tamed the asynchronous beast. Well.... for now at least. This will keep confusing even seasoned JavaScript developers once in a while, so don't worry if it takes some time to get used to.


You need to think async:

.service('userService', [function() {
    this.getUsers = function() {
        var users;
        var ref = firebase.database().ref('/users/');

        <!-- this happens ASYNC -->
        ref.once('value', function(snapshot) {
            users = snapshot.val();
            for(var key in users) {
                users[key].id = key;
                // do some other stuff
            }
            console.log(users); // outputs all users
        },function(error){
            alert('error:  ' + error);
        });
        <!-- end async action -->

        console.log(users); // outputs 'undefined'
    }
}]);

I bet if you did this, you'd be fine:

.service('userService', [function() {
    this.getUsers = function() {
        var users;
        var ref = firebase.database().ref('/users/');

        <!-- this happens ASYNC -->
        ref.once('value', function(snapshot) {
            users = snapshot.val();
            for(var key in users) {
                users[key].id = key;
                // do some other stuff
            }
            console.log(users); // outputs all users
        },function(error){
            alert('error:  ' + error);
        });
        <!-- end async action -->

        window.setTimeout( function() {
            console.log(users); // outputs 'undefined'
        }, 1500 );
    }
}]);

Clarification per Franks's comment:

I should clarify further that the setTimeout would just prove that this is an issue of timing. If you use setTimeout in your app, you're probably going to have a bad time as you can't reliably wait n milliseconds for results, you need to get them then fire a callback or resolve a promise after you have gotten the snapshot of the data.