JavaScript Promises: handle repeatedly rejected requests

by ilyavf

Lets look at JavaScript promises and try to build a simple flow that should handle multiple rejection of the same request.

Why? When I first learned about promises in Javascript I was so excited that decided to use them everywhere: for data, business logic (flow), basically for everything that requires (or potentially could require) async actions. I don’t have strong Node.js experience but I guess folks with this background deal with promises all the time. What I am not sure for now is how flexible and convenient promises are. When I first started to work with event streams (Flapjax, RxJS) I felt the power and could see a lot of advantages in defining a flow using them as well as building data abstraction. But can we rely on promises the same way?

GOAL: By starting with this post what I want is to look at different cases to use promises for and to see how well they work out. I will also try to compare promise based implementation with event stream based one (using RxJS).

If you are not familiar with promises in JS I would recommend you reading the following posts: JavaScript Promises and Promise Anti-patterns

Here is a simple scenario we will implement:

GIVEN: A view with a single Login button.

WHEN User clicks Login
AND  fails authorization
THEN User should be able to click Login and attempt 
     another authorization.

WHEN User clicks Login
AND  succeeds authorization
THEN every next click should skip authorization call and 
     just perform the success action.

*** In my code I will be using Dependency Injection pattern and the implementation of javascript promises by Kris Kowal from here.

We can have our user resource to look like this:

function User (asyncLogin, q) {
    var loginStatus;

    return {
        isLoggedIn: function () {
            if (!loginStatus) {
                loginStatus = q.defer();
                asyncLogin(loginStatus);
            }
            return loginStatus.promise;
        }
    };
}

And lets use a timeout for our asynchronous login functionality:

function asyncLogin (deferred) {
    timeout(function () {
        if (confirm('Do you want to be logged in?')) {
            deferred.resolve(true);
        } else {
            deferred.reject('User did not authorize');
        }
    }, 100);
}

Let our UI look like this: single “Login” button with onClickLogin handler returned by LoginHandler factory:

function LoginHandler (user) {
    return function () {
        user.isLoggedIn().then(function () {
            alert('Successfully Logged in');
        }, function (reason) {
            alert('Error: ' + reason);
        });
    }
}

Now if we initialize our mini app:

    var user = User(asyncLogin, Q),
        onClickHandler = LoginHandler(user);

    $('#loginBtn').click(onClickHandler);

then click on the Login button and login successfully, then all other clicks will alert “Success…” immediately.
But if the first time we fail to login, then all further clicks will alert the error (also immediately).

This is because once we created loginStatus promise (which could be rejected or resolved only once) isLoggedIn() is no longer capable to perform another asyncLogin attempt.

Lets fix this.

First, lets modify the async login function to have its own deferred object:

function asyncLogin () {
    var deferred = q.defer();
    timeout(function () {
        if (confirm('Do you want to be logged in?')) {
            deferred.resolve(true);
        } else {
            deferred.reject('User did not authorize');
        }
    }, 100);
    return deferred.promise;
}

And lets update our api function like this:

function User (asyncLogin, q) {
    var loginStatus;

    return {
        isLoggedIn: function () {
            if (!loginStatus) {
                loginStatus = q.defer();
                asyncLogin().then(function (result) {
                    loginStatus.resolve(result);

                }, function (reason) {
                    // Notify subscribers about the failure:
                    loginStatus.reject(reason);

                    // And destroy the deferred object for this failed attempt:
                    loginStatus = null;
                });
            }
            return loginStatus.promise;
        }
    };
}

Now if we start with unsuccessful login, every time we click Login button for the next attempt user.isLoggedIn() will create a new deferred and perform another async login.
And Once we succeed in login then user.isLoggedIn() will immediately return the resolved promise without async login call.

Here is a JSFiddle (http://jsfiddle.net/UYb6e/) and all the code together along with html is below. Later I will also provide a github link to angularjs app using the described pattern.

CONCLUSION

As we can see promises can handle the described scenario. In terms of reactive programming what we did is we merged two event streams: UI interactions (button click to initiate login) and authorization requests (e.g. an ajax call to a server api). We did have to think of a way of “merging” the streams – we created one series of promise for the login button action and another series of promise for the async call.

<button id="loginBtn">Login</button>

<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/q.js/0.9.2/q.js"></script>

<script>
console.log('Started');
$(function () {
    console.log('Loaded. Initializing');
    var asyncLogin = AsyncLogin(Q),
        user = User(asyncLogin, Q),
        onClickHandler = LoginHandler(user);

    $('#loginBtn').click(onClickHandler);
});

function User(asyncLogin, q) {
    console.log('User initialized');
    var loginStatus;

    return {
        isLoggedIn: function () {
            if (!loginStatus) {
                loginStatus = q.defer();
                asyncLogin().then(function (result) {
                    loginStatus.resolve(result);

                }, function (reason) {
                    // Notify subscribers about failure:
                    loginStatus.reject(reason);

                    // And destroy the deferred object for this failed attempt:
                    loginStatus = null;
                });
            }
            return loginStatus.promise;
        }
    };
}

function AsyncLogin(q) {
    console.log('AsyncLogin initialized with q: ' + typeof q);
    return function () {
        var deferred = q.defer();
        setTimeout(function () {
            if (confirm('Do you want to be logged in?')) {
                deferred.resolve(true);
            } else {
                deferred.reject('User did not authorize');
            }
        }, 100);
        return deferred.promise;
    }
}

function LoginHandler(user) {
    console.log('LoginHandler initialized with user: ' + typeof user);
    return function () {
        user.isLoggedIn().then(function () {
            alert('Successfully Logged in');
        }, function (reason) {
            alert('Error: ' + reason);
        });
    }
}
</script>
Advertisements