0

I created the following two functions to get a JWT Token from my backend and store it in localStorage. The logic seems pretty simple but it's not working because before the value of my token is resolved, the code continues executing. As a result, functions in the chain that need the token don't get it. Ultimately, it does get the token and stores it in localStorage but by that time all my functions in the start up chain fail to get a valid token.

I'm doing all of this at the very beginning as my React app is starting up. So the idea is to get the token and then execute the remaining start up steps.

So, I'm calling myStartup() function at the entry point to my app. I do this as the first step. From there, the remaining start up functions are passed as a callback.

export const getJwtTokenFromApi = () => {

    var request = new Request('/api/token', {
        method: 'GET',
        mode: 'cors',
        credentials: 'include'
    });

    fetch(request)
        .then((response) => {
            response.text()
            .then((token) => {
                if(token.length > 0) {
                    localStorage.setItem('myToken', token);
                    return token;
                } else {
                    return null;
                }
             })
        })
        .catch(err => {
        });
}

export const getJwtToken = () => {

    let token = localStorage.getItem('myToken');

    if (token == null)
        token = getJwtTokenFromApi();

    return token;
}

export const myStartup = (callback) => {

    const token = getJwtToken();
    callback(token);
}

What ends up happeningis that the calling function that needs the token is not deferred until the token is received so it ends up receiving undefined.

How do I make sure I defer the execution of my function that needs the token until I have a legit token -- or at least a null value which would mean my API call failed?

Sam
  • 26,817
  • 58
  • 206
  • 383
  • "*I thought using .then() would actually force the code to wait till the promise is resolved and the result can be consumed.*" - No, it does not block. All it does is to return a *new promise* for the result of the callback. – Bergi Jan 06 '18 at 20:15
  • "*How do I make sure that token is always returned by getJwtToken() function?*" - [that's not possible](https://stackoverflow.com/q/14220321/1048572). Instead, always return a promise for the token. (Oh, and don't use callback parameters) – Bergi Jan 06 '18 at 20:15
  • @Bergi The link you sent suggests using callbacks. I'm actually using a callback just to make sure by the time I hit `storeToken()` function, I have a token but for some reason, I'm calling `storeToken()` with an `undefined` token. I don't know why that's happening. – Sam Jan 06 '18 at 20:18
  • I also tried `async/await` though not sure if I did it right. That didn't help either though I was sure it would fix my issue. I use `async/await` in C# all the time and it works great. – Sam Jan 06 '18 at 20:19
  • I think you should call callback(response.text()) inside the first .then – Marouen Mhiri Jan 06 '18 at 20:20
  • The linked answers also suggest to use promises. I would recommend that you master `then` before using `await` as syntactic sugar for it. – Bergi Jan 06 '18 at 20:20
  • @MarouenMhiri I'm not if I understand your suggestion correctly. Could you please share code? – Sam Jan 06 '18 at 20:21
  • @Bergi I agree that `async/await` is just syntactic sugar but admittedly I find the whole chaining of `.then()`s terribly confusing because the code gets so verbose and indentations all over the place, it becomes hard to read it. So I'll take syntactic sugar by the gallons! – Sam Jan 06 '18 at 20:23
  • "*Code jumps back to getJwtTokenFromApi()*" - no, it doesn't jump around. It goes into the `then` callback once the fetch request succeeded. And step 7 ("*After this, code goes back to the original function*") does not happen at all. It went there in step 4 already. It won't do it again. – Bergi Jan 06 '18 at 20:24
  • 1
    @Sam Then please also show us how you tried to use async/await. Don't forget that all async functions immediately return a promise! – Bergi Jan 06 '18 at 20:25
  • @Bergi using the code I posted above, what would you suggest I change? I don't want to keep trying different approaches. I thought the code i posted shows a fairly simple logic. Why am I not hitting the callback with an actual value but `undefined` instead? – Sam Jan 06 '18 at 20:28
  • 1
    The `(token) => { callback(token); }` does receive `undefined` because the promise returned by the `.then()` before resolved with the result of its callback, which doesn't return anything. Add a `return` in front of the `response.text()`. Notice that this will call the `callback` (`storeToken`) with the expected value (step 5/6), but it cannot make `getJwtTokenFromApi` return the token (step 3/4). – Bergi Jan 06 '18 at 20:32
  • or just call the callback directly on the response of the first then – Marouen Mhiri Jan 06 '18 at 20:34
  • @Bergi You're definitely right. Adding the `return` actually fixed the problem with `undefined` value when I hit the `storeToken()` function. Howeer, the code still goes back to the calling function with an `undefined` token value though because it keeps executing before the token is resolved and stored. – Sam Jan 06 '18 at 20:38
  • @Sam I think I previously told you that that's not possible. `token` is not "resolved", a promise can be resolved with a value. And you cannot block until a promise is resolved, you can only defer any following code. – Bergi Jan 06 '18 at 20:45
  • @Bergi I understand that the execution of the calling method needs to be deferred until `getJwtToken()` gets done. I changed the logic a bit so that I send the calling code as a callback. I updated my code in original post. I'm still getting `undefined` for token. It seems like I'm not able to execute the resolution of promises THEN move on to the next step. – Sam Jan 07 '18 at 00:11

3 Answers3

1

Your function getJwtToken should return the promise:

export const getJwtToken = () => {   
  let token = localStorage.getItem('myToken');
  return token ? Promise.resolve(token) : getJwtTokenFromApi(storeToken) 
}

in your caller, the token will be wrapped in the inside returned promise:

getJwtToken().then(token => doSomething(token))
Gideon Oduro
  • 200
  • 1
  • 2
  • 15
  • Then `getJwtTokenFromApi` would need to return a promise as well. – Bergi Jan 06 '18 at 20:45
  • Yes thats true, you can checkout my example where i'm using the ternary operator to return promise from local storage or the api call. – Gideon Oduro Jan 06 '18 at 20:49
  • @GideonOduro I updated the code as per your suggestion but in the calling function, I'm getting **Uncaught TypeError: Cannot read property 'then' of undefined** error. – Sam Jan 06 '18 at 21:25
1

The fundamental problem is that you're not handling all the async stuff as though it's async. This is a reasonably complicated workflow, with blocking and non-blocking tasks intermixed, which means that we need to apply asynchronous patterns throughout. Let's step through the script one function at a time.


This appears to be the script's entry point:

export const myStartup = (callback) => {
    const token = getJwtToken();
    callback(token);
}

It won't work, because getJwtToken is async, which means that its value will not be available to callback on the next line.

How do we know that getJwtToken is async? Because getJwtToken invokes getJwtTokenFromApi, which invokes fetch (which the spec tells us is async). Since getJwtToken wraps async behavior, it is itself async.

Since getJwtToken is async, we know that token is not going to be available on the second line when callback needs it. In fact, token will never be available in that scope, because getJwtToken returns a Promise, whose resolution value will only be available inside a .then handler. So, step 1 is to rewrite this function:

export const myStartup = (callback) => {
    getJwtToken() // returns a Promise
    .then((token) => { // token is available inside Promise's .then
        callback(token);
    })
}

Now we look inside getJwtToken, bearing in mind that it must return a Promise because of the changes we just made.

export const getJwtToken = () => {
    let token = localStorage.getItem('myToken');
    if (token == null)
        token = getJwtTokenFromApi();
    return token;
}

This is an interesting case, because getJwtToken implements branching behavior, one branch of which is synchronous, and the other not. (localStorage.getItem blocks, but getJwtTokenFromApi is async.) The only way to handle cases like this is to make the entire function async: to make sure that it always returns a Promise, even if the data it needs is available from a sync source.

Since localStorage.getItem is synchronous, if we like the value it gives us, we wrap that value in a Promise before returning it. Otherwise, we can just return the Promise returned by getJwtTokenFromApi:

export const getJwtToken = () => {
    let token = localStorage.getItem('myToken')

    if(token !== null)
        return Promise.resolve(token);

    return getJwtTokenFromApi();
}

Now, no matter which scenario we find ourselves in, this function will return a Promise that contains a token.

Finally, we get to getJwtTokenFromApi, which does a few things:

  • it constructs a Request
  • it executes a request (async)
  • if successful, it converts the response to text (async)
  • it inspects the text

If all those things work out, it wants to return the text value. But half of those tasks are async, which again means that the entire function must become async. Here's a slimmer version of what you started with:

export const getJwtTokenFromApi = () => {

    var request = new Request('/api/token', {});

    fetch(request)
    .then((response) => {
        response.text()
        .then((token) => {
            if(token.length > 0) {
                localStorage.setItem('myToken', token);
                return token;
            } else {
                return null;
            }
         })
    })
}

The biggest problem here is that you're not returning the fetch. This is important, because the other return statements nested inside don't apply to the overall function. This function will not return anything as written, although it will perform an XHR call. So, the first fix is to return fetch.

But just adding that return isn't enough. Why? Because within the .then handler, you want to access the text of the response, but that access is itself async. While you are using a .then to access the value (as token), that value will die silently inside fetch.then unless you also return response.text(). Really, what you need is this:

return fetch(request)
.then((response) => {
    return response.text()
    .then((text) => {
        if(text.length > 0) return text;
        else return null

But this code is needlessly verbose, and the way it creeps to the right with deeper and deeper nesting makes for code that is hard to read or re-order. These steps are sequential, and we want them to look that way:

STEP 1
STEP 2
STEP 3

(not)
STEP 1
    STEP 2
        STEP 3

So, let's try something slimmer:

return fetch(request)                          // step 1
.then((response) => response.text())           // step 2
.then((text) => text.length > 0 ? text : null) // step 3

This code is flatter and slimmer. It's also easier to re-order the steps or insert new ones. Of course, it doesn't do the important work of storing the token in localStorage, which is why we have the slightly beefier final version:

export const getJwtTokenFromApi = () => {

    var request = new Request('/api/token', {
        method: 'GET',
        mode: 'cors',
        credentials: 'include'
    });

    return fetch(request)
    .then((response) => response.text())
    .then((token) => {
        if(token.length > 0) {
            localStorage.setItem('myToken', token);
            return token;
        }

        return null;
     })
    })
}

We're able to flatten all this code because of the way nested Promises resolve: when one Promise contains another Promise (and another, etc.), the engine will automatically unwrap all of the intermediate promises. As an example, these two snippets produce identical results:

var x = Promise.resolve( Promise.resolve( Promise.resolve ( 10 )))
var y = Promise.resolve( 10 )

Both x and y will act like single, flat Promises that resolve to 10, which means we can put this after either one:

.then((value) => {
    // value === 10
})

Here's the final script:

export const getJwtTokenFromApi = () => {

    var request = new Request('/api/token', {
        method: 'GET',
        mode: 'cors',
        credentials: 'include'
    });

    return fetch(request)
    .then((response) => response.text())
    .then((token) => {
        if(token.length > 0) {
            localStorage.setItem('myToken', token);
            return token;
        }

        return null;
     })
    })
}

export const getJwtToken = () => {
    let token = localStorage.getItem('myToken')

    if(token !== null)
        return Promise.resolve(token);

    return getJwtTokenFromApi();
}

export const myStartup = (callback) => {
    getJwtToken()
    .then((token) => {
        callback(token);
    })
}

One more question: is myStartup async or not?

Using the rule of thumb from above, we'd say that since it wraps async behavior, it is itself async. However, this script mixes async patterns: both Promises & callbacks. I suspect this is because you are more familiar with node-style callbacks one the one hand, but fetch returns a Promise, and during implementation those two approaches kind of "meet in the middle" -- or rather, at the module's API: myStartup. It's an async function, but it doesn't seem comfortable with that fact.

When a caller invokes myStartup, it will return nothing. That much is obvious because there is no return statement. But, by accepting a callback function, you've provided a mechanism to signal callers once all the potentially-async work is complete, which means it can still be used.

Unless it's important to support the node-style callback pattern, I'd recommend taking the final step to make this module thoroughly Promise-based: modify myStartup so that it returns a Promise that resolves with the token. Because of the aforementioned unwrapping behavior, this is an extremely simple change:

export const myStartup = () => {
    return getJwtToken();
}

But now it's obvious that myStartup adds nothing to the process, so you might as well remove the wrapper by deleting the function and renaming getJwtToken to myStartup.

Tom
  • 8,509
  • 7
  • 49
  • 78
  • 1
    Thank you very much for this fantastic explanation. I so appreciate your taking the time to dissect the code patiently and providing clear explanations along the way. Everything you've explained makes perfect sense! Thank you! – Sam Jan 07 '18 at 17:33
  • Glad to hear it! FWIW: every year I find myself saying "I finally understand Promises." Async patterns is a complicated problem domain. We didn't even touch on error handling -- if the API fails, what code path will be executed? – Tom Jan 07 '18 at 17:47
0

I got this working with the following code but I don't think it's very elegant.

Essentially, I combined multiple functions into one and added a callback parameter so that I can create a chain of start up functions. If no callback is received, I simply return the token in order to make the getJwtToken() a multi purpose function i.e. either call it to get the token or pass a function that expects the token.

I really would like to have separate functions so that not all concerns are in one function. Also, not crazy about having callback parameter for those times when I need to just get the token.

I wanted to post the code so that I can get some suggestions to make it more robust and elegant.

export const getJwtToken = (callback) => {

    // If token is already in localStorage, get it and return it.
    const token = localStorage.getItem('myToken');
    if (token != null)
        return token;

    // Token is not in localStorage. Get it from API
    var request = new Request('/api/token', {
        method: 'GET',
        mode: 'cors',
        credentials: 'include'
    });

    fetch(request)
        .then((response) => {

            response.text()
                .then((token) => {

                    if (token.length > 0) {

                        // First, save it in localStorage
                        localStorage.setItem('myToken', token);

                        // If no callback function received, just return token
                        if (typeof callback == "undefined") {

                            return token;
                        } else {

                            callback(token);
                        }
                    }
                })
        })
        .catch(err => {
        });
}
Sam
  • 26,817
  • 58
  • 206
  • 383
  • Now stop taking callbacks to your functions and start returning promise objects instead. The call changes from `getJwtToken(callback)` to `getJwtToken().then(callback)` – Bergi Jan 07 '18 at 10:24