How to fetch response body using fetch in case of HTTP error 422?
Short answer
Because your assumption that you parse the response content twice is incorrect. In the original snippet, the then
after the then(checkStatus)
is skipped when the error condition is met.
Long answer
In general, well-structured promise chain consists of a:
- Promise to be fulfilled or rejected sometime in the future
then
handler that is run only upon fulfillment of the Promisecatch
handler that is run only upon rejection of the Promisefinally
(optional) that is run when a Promise is settled (in either 2 or 3)
Each of the handlers in 2 and 3 return a Promise to enable chaining.
Next, fetch
method of the Fetch API rejects only on network failures, so the first then
method is called regardless of the status
code of the response. Your first handler, the onFulfilled
callback returns either a fulfilled or rejected Promise.
If fulfilled, it passes control to the next then
method call in the chain, where you extract JSON by calling json
method on the response, which is then passed as Promise value to the last then
method to be used in the successCallback
.
If rejected, the control is passed to the catch
method call, which receives the Promise with the value set to new Error(response)
that you then promptly pass to errorCallback
. Therefore, the latter receives an instance of Error
, whose value is an instance of Response
from Fetch API.
That is exactly what you see logged: Error: [object Response]
, a result of calling toString
method on an instance of Error
. The first part is the constructor name, and the second is a string tag of the contents (of the form [type Constructor]).
What to do?
Since your API returns JSON response for every possible use case (201
, 404
, 422
), pass the parsed response to both fulfilled and rejected promise. Also, note that you accidentally declared checkStatus
on the global scope by omitting a var
, const
, or let
keyword:
//mock Response object
const res = {
status: 200,
body: "mock",
async json() {
const {
body
} = this;
return body;
}
};
const checkStatus = async (response) => {
const parsed = await response.json();
const {
status
} = response;
if (status >= 200 && status < 300) {
return parsed;
}
return Promise.reject(new Error(parsed));
};
const test = () => {
return checkStatus(res)
.then(console.log)
.catch((err) => console.warn(err.message))
.finally(() => {
if (res.status === 200) {
res.status = 422;
return test();
}
});
};
test();
Additionally, since you already use ES6 features (judging by the presence of arrow functions), why not go all the way and use the syntactic sugar async
/await
provides:
(() => {
try {
const response = await fetch("api_url_here", {
method: 'some_method_here',
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
'X-CSRF-Token': "some_token_here"
}
});
const parsed = await response.json(); //json method returns a Promise!
const {
status
} = response;
if (status === 201) {
return successCallback(parsed);
}
throw new Error(parsed);
} catch (error) {
return errorCallback(error);
}
})();
Note that when you are passing the parsed
JSON content to the Error()
constructor, an abstract ToString
operation is called (see step 3a of the ECMA spec).
If the message is an object, unless the original object has a toString
method, you will get a string tag, i.e. [object Object]
, resulting in the content of the object being inaccessible to the error handler:
(() => {
const obj = { msg : "bang bang" };
const err = new Error(obj);
//will log [object Object]
console.log(err.message);
obj.toString = function () { return this.msg; };
const errWithToString = new Error(obj);
//will log "bang bang"
console.log(errWithToString.message);
})();
Contrary, if you reject
a Promise with the object passed as an argument, the rejected Promise's [[PromiseValue]]
will be the object itself.
It's because in case of an error then handlers are skipped and the catch handler is executed, checkStatus
returns a rejected promise and the remaining then
chain is bypassed.
This is how I'd refactor your code.
checkStatus = async (response) => {
if (response.status >= 200 && response.status < 300)
return await response.json()
throw await response.json()
}
}
fetch("api_url_here", {
method: 'some_method_here',
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
'X-CSRF-Token': "some_token_here"
}
})
.then(checkStatus)
.then(successCallback)
.catch(errorCallback);
P.S. this code can be made a bit better to look at visually by using async/await