JakeChampion/fetch

Stop throwing errors and wrap JSON.parse with try/catch block

niftylettuce opened this issue · 13 comments

I believe it's correct as per the spec for fetch to throw an error if you try to get json output from a non json response… ?

so if that were the case, how do you gracefully show the user an error
message when the error doesn't get bubbled up to the callback/promise? now
im having to write try/catch blocks everywhere.
On Nov 26, 2015 4:51 PM, "Matt Andrews" notifications@github.com wrote:

I believe it's correct as per the spec for fetch to throw an error if you
try to get json output from a non json response… ?


Reply to this email directly or view it on GitHub
#235 (comment).

e.g. an endpoint suddenly returns 404 Not Found instead of normal 200 JSON
On Nov 26, 2015 4:52 PM, "Nick Baugh" niftylettuce@gmail.com wrote:

so if that were the case, how do you gracefully show the user an error
message when the error doesn't get bubbled up to the callback/promise? now
im having to write try/catch blocks everywhere.
On Nov 26, 2015 4:51 PM, "Matt Andrews" notifications@github.com wrote:

I believe it's correct as per the spec for fetch to throw an error if you
try to get json output from a non json response… ?


Reply to this email directly or view it on GitHub
#235 (comment).

Because of this mess, here's the fix I wrote for this issue we have now... I basically have to do my own JSON.parse:

        fetch(apiUri + path, {
          method: 'GET',
          headers: that.headers
        })
        .then((res) => {
          try {
            let response = JSON.parse(res);
            if (response && response.error)
              throw new Error(response.error);
            return res.json();
          } catch (e) {
            return res.text();
          }
        })
        .then((res) => {
          fn(null, res);
        })
        .catch(function(err) {
          fn(err);
        })

We normally check the status (res.ok is handy for that) of the response before requesting json…

.then(res => {
  if (res.ok) {
    return res.json();
  } else {
    // handle error 
  }
});

Also you could always implement the try catch yourself and do:-

.then(res => res.text())
.then(res => {
  try {
    return JSON.parse(res);
  } catch(err) {
    // handle error
  }
});

I would highly recommend checking the status of the response either via .ok or .status.

good point

I will close this issue then :)

@matthew-andrews Attempting to return response.json() after reading response.ok (or response.status) throws 'Already read' log and 'undefined is not an object' error

@sethfatz Make sure to return response from a handler that checks response.ok.

Does anyone have any "real world" examples of how they're handling errors here? Particularly in react?

.then(res => res.text())
.then(res => {
  try {
    return JSON.parse(res);
  } catch(err) {
    // handle error
    // return error and use this.state to display it?
    // show an alert?
   // alert sentry
  }
});

I'm sure it's context specific, but I'm having trouble finding good examples beyond error handling 101.

Pretty sure I can share this without giving away too many trade secrets 😉

Premise: a successfuly query to our API endpoint returns a valid JSON packet
Sometimes it returns an error packet with a suitable code e.g. 409 - conflict
Sometimes it returns a 404 (e.g. record not found)
Sometimes it returns an empty file, or text, or html (it's complicated)

I definitely lifted some of it from a helpful tutorial - will cite if I can find it, but worked on the error handler a lot since then

const fetchJSON = ((url, apiKey) => {
  return fetch(url, {
    mode: 'cors',
    credentials: 'include',
    cache: 'no-store',
    headers: {
      'Bearer': apiKey,
    }
  }).then(validateResponse)
    .then(readResponseAsJSON)
    .catch(handleResponseError)
})

const validateResponse = (response => {
  if (!response.ok) {
    return Promise.reject(response)
  }
  return response
})

const readResponseAsJSON = (response => {
  return response.json()
})

const handleResponseError = (response => {
  // explicitly returning a promise, because I can't figure out the right syntax otherwise
  return new Promise((resolve, reject) => {
    const statusText = response.status + ': ' + response.statusText
    response.text()  //response.json() doesn't appear to support sensible error handling of non-JSON
      .then(text => {
        let jsonData = {}
        try {
          jsonData = JSON.parse(text)  // try to do our own parsing of the json here
        } catch(err) {
          //no-op
        }
        return jsonData
      })
      .then(packet => {
        if (packet.error) {
          return reject(packet.error)
        } else {
          if (response.status == 404) {
            return reject('404: not found')  //we treat this as a special code in a couple of places
          } else {
            return reject(statusText)
          }
        }
      })
  })
})

We use that everywhere, in the format

fetchJSON('http://example.com/json', 'mySuperSecretApiKey123')
  .then(packet => {
    //success
    console.log(packet)
  }).catch(err => {
    //failure
    console.error(err)
  })

I think there is a good real-world solution@MDN to distinct JSON-formatted response and plain text.

fetch(myRequest).then(function(response) {
    var contentType = response.headers.get("content-type");
    if(contentType && contentType.includes("application/json")) {
      return response.json();
    }
    throw new TypeError("Oops, we haven't got JSON!");
  })
  .then(function(json) { /* process your JSON further */ })
  .catch(function(error) { console.log(error); });