DEV Community

Farah Anjum
Farah Anjum

Posted on • Edited on

Callbacks & Promises in Javascript

Javascript is a single threaded asynchronous language. What does that mean? It means it cannot multi-task, but can pause execution of one task to do something else and switch back and forth between tasks so as to maximise performance. It makes sense because javascript is a browser language, and browsers need to make network calls which are one of the biggest bottlenecks to the speed of any web app.

Many times we want to do things where the result of one function determines the output of the next function and so on. For example, suppose we want to log a user in, given their username and password. So first, we check if the username and password match, only if they do, we fetch the user's profile, and only if the user's profile is correctly fetched, we log them in. If at any step the process fails, we don't execute the next step and call it off.

To achieve this, developers conventionally used to use the concept of callback functions. As the name suggests, callback functions are only executed when the execution of the previously called function gets over.

So to achieve our objective of logging in the user, we'd do something like this:

getUser(username, password, (err, user) => {
    if(err) console.log("Error fetching the user");
    else {
        getUserProfile(user, (err, profileData) => {
            if(err) console.log("error fetching user profile");
            else {
                logUserIn(profileData, (err, result) => {
                    if(err) console.log("Error logging in the user");
                    else console.log("User successfully logged in");                                  
                })
            }
        })
    }
});

 getUser = (username, pwd, callback) => {
    let user = db.getUser(username, pwd);
    if(user) callback(null, user);
    else callback(new Error("username and pwd dont match"));
}

getUserProfile = (user, callback) => {
    let profileData = db.getUserProfile(user);
    if(profileData) callback(null, profileData);
    else callback(new Error("Profile couldnt be fetched"));
}

There are two problems with this approach. First, to understand the logic, we need to move up and down the code several times. The code doesn't read linearly. Also, the code suffers from a condition developers colloquially refer to as the 'callback hell'. If we were to perform several more steps after logging in the user, and if all of them were asynchronous each having its callback, you can imagine what the upper code block would look like. Not pretty. Also notice the triangular white space which keeps increasing as we keep going down the hell. It is fondly called the pyramid of doom. Nobody likes to waste that much space, especially when it acts opposite to readability, as opposed to its originally intended purpose.

The second and actual problem (imo) with this code is that it violates DRY principle several times. Notice how the error handling is repeated in each callback function.

So to overcome these two problems of readability and lack of brevity, the concept of promises was introduced:

getUser(username, password)
    .then(user => {
        return getUserProfile(user);
    })
    .then(profileData => {
        return logUserIn(profileData);
    })
    .then(
        console.log("User successfully logged in")
    )
    .catch(err => 
        console.log(err)
    )

getUser = (username, password) => {
    return new Promise((resolve, reject) => {
        let user = db.getUser(username, password);
        if (user) resolve(user);
        else reject(new Error("Username and pwd do not match"));
    });
}

getUserProfile = (user) => {
    return new Promise((resolve, reject) => {
        let profileData = db.getUserProfile(user);
        if (profileData) resolve(profileData);
        else reject(new Error("User profile could not be fetched"));
    });
}

The execution of functions looks linear: we getUser() first, then we getUserProfile(), then logUserIn(), and if any error happens in any of the steps, we catch it with a single catch. Promises have this concept called error propagation, where error happening at any step will cause the execution to jump to the catch statement. So one catch suffices all.

getUser() and getUserProfile() return a Promise, which means when we call these two functions, they make us wait (since they are going to perform time taking async operations), and "promise" us that they will return something, and will not eternally keep us waiting. If the async operation that these functions perform is successful, they "resolve", or return the value and we move on to the next then, else they reject the error which will directly go to the catch statement.

So there it is: promises just act as a syntactical sugar to the conventional callback approach, with the added benefit of Error Propagation which keeps us from repeating error-catching. Some say that the new 'async-await' paradigm in JS is what's truly advantageous over these two approaches, but we'll cover that some other day :)

Top comments (3)

Collapse
 
zaffja profile image
Zafri Zulkipli

Splendid explanation and example! I feel more confident in using Promises already.

Collapse
 
angch profile image
Ang Chin Han

Nitpick. Standardize your indentation spacing, use an autoformatter where possible.

Collapse
 
farahanjum profile image
Farah Anjum

Thanks for pointing out, corrected :)