67

I have an HTTP API that returns JSON data both on success and on failure.

An example failure would look like this:

~ ◆ http get http://localhost:5000/api/isbn/2266202022 
HTTP/1.1 400 BAD REQUEST
Content-Length: 171
Content-Type: application/json
Server: TornadoServer/4.0

{
    "message": "There was an issue with at least some of the supplied values.", 
    "payload": {
        "isbn": "Could not find match for ISBN."
    }, 
    "type": "validation"
}

What I want to achieve in my JavaScript code is something like this:

fetch(url)
  .then((resp) => {
     if (resp.status >= 200 && resp.status < 300) {
       return resp.json();
     } else {
       // This does not work, since the Promise returned by `json()` is never fulfilled
       return Promise.reject(resp.json());
     }
   })
   .catch((error) => {
     // Do something with the error object
   }
sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
jbaiter
  • 6,913
  • 4
  • 30
  • 40

5 Answers5

102
 // This does not work, since the Promise returned by `json()` is never fulfilled
return Promise.reject(resp.json());

Well, the resp.json promise will be fulfilled, only Promise.reject doesn't wait for it and immediately rejects with a promise.

I'll assume that you rather want to do the following:

fetch(url).then((resp) => {
  let json = resp.json(); // there's always a body
  if (resp.status >= 200 && resp.status < 300) {
    return json;
  } else {
    return json.then(Promise.reject.bind(Promise));
  }
})

(or, written explicitly)

    return json.then(err => {throw err;});
cn007b
  • 16,596
  • 7
  • 59
  • 74
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks, that (almost) did work! I had to wrap Promise.reject in an anonymous function or I'd get an `undefined is not a function` error, but with that small change it works :-) – jbaiter Apr 07 '15 at 08:10
  • Uh, are you lazy-loading a Promise shim? The native `Promise.reject` should never be `undefined`. – Bergi Apr 07 '15 at 10:09
  • The shim is loaded on application startup, so it shouldn't be lazy-loaded. I can access `Promise.reject` just fine from the debugger, too. Here's the full trace: `TypeError: undefined is not a function {stack: "TypeError: undefined is not a function↵ at reject (native)", message: "undefined is not a function"}` – jbaiter Apr 07 '15 at 11:45
  • 2
    At reject? Ah, it needs to be `.then(Promise.reject.bind(Promise))`. – Bergi Apr 07 '15 at 14:00
  • I would seem that `json.then(Promise.reject.bind(Promise))` is equivalent to `Promise.reject(json)`, or am I missing something? –  Sep 08 '15 at 05:33
  • 1
    @torazaburo: No, `json` is a promise here, and we don't want to reject with the promise but with its result value. – Bergi Sep 08 '15 at 10:13
  • There is no body if you get a '204 No Content' response ;) – CpILL Jun 01 '16 at 09:46
  • @CpILL: But still an empty one, isn't there? – Bergi Jun 01 '16 at 14:45
  • @Bergi what if my response has no body? resp.json() will throw an error. How to handle that? – Shabbir Essaji Aug 03 '20 at 17:47
  • @ShabbirEssaji Then move the `resp.json()` call in the `else` part (assuming your errors have bodies) and in the `if` block return a success value. Alternatively use `resp.text()` (which should return an empty string?) or add an `if (resp.headers.get('Content-Length') > 0)` check. – Bergi Aug 03 '20 at 17:57
42

Here's a somewhat cleaner approach that relies on response.ok and makes use of the underlying JSON data instead of the Promise returned by .json().

function myFetchWrapper(url) {
  return fetch(url).then(response => {
    return response.json().then(json => {
      return response.ok ? json : Promise.reject(json);
    });
  });
}

// This should trigger the .then() with the JSON response,
// since the response is an HTTP 200.
myFetchWrapper('http://api.openweathermap.org/data/2.5/weather?q=Brooklyn,NY').then(console.log.bind(console));

// This should trigger the .catch() with the JSON response,
// since the response is an HTTP 400.
myFetchWrapper('https://content.googleapis.com/youtube/v3/search').catch(console.warn.bind(console));
Jeff Posnick
  • 53,580
  • 14
  • 141
  • 167
  • 1
    Ah, `.ok` looks interesting. Yet I don't see usage of "the underlying JSON data" is cleaner. After all, you could simplify this to `fetch(url).then(response => response.ok ? response.json() : response.json().then(err => Promise.reject(err)))` – Bergi Sep 09 '15 at 21:12
  • I meant that instead of `let json = resp.json();` where `json` is a `Promise`, it could be simpler to resolve the `Promise` first and then make use of the data its resolved with. Either approach works. – Jeff Posnick Sep 10 '15 at 13:28
  • Was trying to reject the nested promise but wasn't quite sure how. Turns out it's simply a call to the static "reject" method. A far better answer than the accepted one in my view. – Andris Apr 01 '16 at 20:06
12

The solution above from Jeff Posnick is my favourite way of doing it, but the nesting is pretty ugly.

With the newer async/await syntax we can do it in a more synchronous looking way, without the ugly nesting that can quickly become confusing.

async function myFetchWrapper(url) {
  const response = await fetch(url);
  const json = await response.json();
  return response.ok ? json : Promise.reject(json);
}

This works because, an async function always returns a promise and once we have the JSON we can then decide how to return it based on the response status (using response.ok).

You would error handle the same way as you would in Jeff's answer, however you could also use try/catch, an error handling higher order function, or with some modification to prevent the promise rejecting you can use my favourite technique that ensures error handling is enforced as part of the developer experience.

const url = 'http://api.openweathermap.org/data/2.5/weather?q=Brooklyn,NY'

// Example with Promises
myFetchWrapper(url)
  .then((res) => ...)
  .catch((err) => ...);

// Example with try/catch (presuming wrapped in an async function)
try {
  const data = await myFetchWrapper(url);
  ...
} catch (err) {
  throw new Error(err.message);
}

Also worth reading MDN - Checking that the fetch was successful for why we have to do this, essentially a fetch request only rejects with network errors, getting a 404 is not a network error.

tomhughes
  • 4,597
  • 2
  • 24
  • 33
0

I found my solution at MDN:

function fetchAndDecode(url) {
  return fetch(url).then(response => {
    if(!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    } else {
      return response.blob();
    }
  })
}

let coffee = fetchAndDecode('coffee.jpg');
let tea = fetchAndDecode('tea.jpg');

Promise.any([coffee, tea]).then(value => {
  let objectURL = URL.createObjectURL(value);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e => {
  console.log(e.message);
});
insign
  • 5,353
  • 1
  • 38
  • 35
0

Maybe this option can be valid

new Promise((resolve, reject) => { 
    fetch(url)
    .then(async (response) => {
        const data = await response.json();
        return { statusCode: response.status, body: data };
    })
    .then((response) => {
        if (response.statusCode >= 200 && response.statusCode < 300) {
            resolve(response.body);
        } else {
            reject(response.body);
        }
    })
});
Fdezdam
  • 23
  • 2
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 14 '22 at 00:33