Returning a fetch promise from a catch block

I have a fetch block which integrates to an API and sets the bearer token in the headers. Sometimes when calling the API the bearer token is expired, in this scenario I need to handle the 401 response in catch block by calling a refresh endpoint, and set the access token value in a cookie. After which I can attempt to call the API again by using the new token set in the cookie.

I have the following fetch block

fetch(host + '/api/v1/list', {
        headers: {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": "Bearer " + getBearerToken()
        }
    }).then((response) => {
        return response.json();
    }).then((data) => {
        return data.items;
    }).catch(response => {
        return fetch(host + 'refresh', {
            method: 'POST',
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + getBearerToken()
            }
        });
    }).then((response) => {
        return response.json();
    }).then((data) => {
        let token = data['Access_token'];
        document.cookie="SSO_token=" + token + '; expires=0; path=/';
        return fetch(host + '/api/v1/list', {
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + getBearerToken()
            }
        });
    }).then((response) => {
        return response.json();
    }).then((data) => {
        return data.items;
    })

The console reports a TypeError in the first call to then after the catch

    }).catch(response => {
        return fetch(host + 'refresh', {
            method: 'POST',
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + getBearerToken()
            }
        });
    }).then((response) => {
        return response.json();
    })

response.json is not a function. I’m not understanding the error here. Any help appreciated

  • The response argument coming into the catch block is the error/exception being thrown, not the response of your fetch call

    – 




  • Yes but the issue isn’t in the catch I don’t believe, it’s the proceeding then after returning the fetch from the catch

    – 

  • 1

    Notice you’re not actually handling the 401 response anywhere?

    – 

The immediate issue is the following sequence

(shortened for brevity)

fetch(/* data */)
    .then((response) => { return response.json(); })          // <--+
    .then((data) => { return data.items; })                   //    |
    .catch(response => { return fetch(/* bearer token */); }) //    |
    .then((response) => { return response.json(); })          // <--+

This would only work if the first fetch() fails which would then jump over the first response.json(), go to the .catch() and then continue with the next response.json().

However, if the first fetch() succeeds, then the .catch() would be passed over, as there is no error. This leads to effectively the following code:

const demoResponse = {
  json() { 
    return { items: 42 };
  }
}

const demoFetch = () => { 
  return Promise.resolve(demoResponse);
}

demoFetch(/* data */)
    .then((response) => { return response.json(); }) // calling .json() on the response
    .then((data) => { return data.items; })          // getting some data. 
    //No .catch() because there is no error
    .then((response) => { return response.json(); }) // calling `.json()` on the previous result
    .catch((error) => console.error("and this would always fail", error.message))

Taking a step back, the problem that led to this one is trying to do so many operations in the same sequence of promises. Much easier is to define what needs to be done. Something like

function makeRequest() {
  return fetch(host + '/api/v1/list', {
    headers: {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "Authorization": "Bearer " + getBearerToken()
    }
  });
}

function refreshToken() {
  return fetch(host + 'refresh', {
    method: 'POST',
    headers: {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "Authorization": "Bearer " + getBearerToken()
    }
  }).then((response) => {
    return response.json();
  }).then((data) => {
    let token = data['Access_token'];
    document.cookie="SSO_token=" + token + '; expires=0; path=/';
  })
}

Which can then more easily be composed together and would be more readable and easier to make sure it is correct:

makeRequest()
  .catch(() => { return refreshToken(); } ) // attempt recovery...
  .then(() => { return makeRequest(); })    // ...and retry
  .then((response) => { return response.json(); })
  .then((data) => { return data.items; })

With this code the token refresh is only attempted if the request fails and only once. Moreover, the refresh is itself in a separate function, so it does not need to be handled specially in the promise chain. Including it anywhere will mean the entire refresh process is done before continuing, without needing to try and handle the response of the refresh in the same promise chain as handling the other responses.

The second request can still fail two times in a row. Presumably this would not be due to the access token but for some other reason. At any rate, it is advised to handle other possible failures appropriately.

The second .then(response => …) is called with either the result of the .catch() callback (i.e. the refetch response) or – if there was no error in the first place – with the result of the previous then callback (i.e. the data.items), and the latter value has no .json() method.

You want to run all of that code only if an error occurred, so you need to nest that entire promise chain inside the catch callback:

fetch(host + '/api/v1/list', {
    headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "Authorization": "Bearer " + getBearerToken()
    }
}).then((response) => {
    return response.json();
}).then((data) => {
    return data.items;
}).catch(response => {
    return fetch(host + 'refresh', {
        method: 'POST',
        headers: {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": "Bearer " + getBearerToken()
        }
    }).then((response) => {
        return response.json();
    }).then((data) => {
        let token = data['Access_token'];
        document.cookie="SSO_token=" + token + '; expires=0; path=/';
        return fetch(host + '/api/v1/list', {
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + getBearerToken()
            }
        });
    }).then((response) => {
        return response.json();
    }).then((data) => {
        return data.items;
    });
});

However, the catch is only handling network errors and json parsing errors, but not the http errors that you actually want to handle by retrying the request with a new token. To achieve that, you need to explicitly check the status of the response:

fetch(host + '/api/v1/list', {
    headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "Authorization": "Bearer " + getBearerToken()
    }
}).then((response) => {
    if (response.ok) {
        return response.json().then((data) => {
            return data.items;
        });
    } else if (response.status == 401) {
        return fetch(host + 'refresh', {
            method: 'POST',
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + getBearerToken()
            }
        }).then((response) => {
            if (response.ok) {
                return response.json();
            } else {
                throw new Error(response.statusText);
            }
        }).then((data) => {
            let token = data['Access_token'];
            document.cookie="SSO_token=" + token + '; expires=0; path=/';
            return fetch(host + '/api/v1/list', {
                headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/json",
                    "Authorization": "Bearer " + getBearerToken()
                }
            });
        }).then((response) => {
            if (response.ok) {
                return response.json();
            } else {
                throw new Error(response.statusText);
            }
        }).then((data) => {
            return data.items;
        });
    } else {
        throw new Error(response.statusText);
    }
});

Leave a Comment