I'm looking for a way of handling errors with the native javascript fetch api. Used to use jQuery, but I'm trying to use more native javascript functions.
I found this blog and like the approach: https://learnwithparam.com/blog/how-to-handle-fetch-errors/
fetch(url)
.then((response) => {
if (response.status >= 200 && response.status <= 299) {
return response.json();
}
throw Error(response.statusText);
})
.then((jsonResponse) => {
// do whatever you want with the JSON response
}).catch((error) => {
// Handle the error
console.log(error);
});
However, in the catch I'm getting the statusText that belongs to the HTTP code. For 400 for example Bad request. But that is not wat I want, my call to the server will respond with exactly what is wrong. So I want to use the response body text as a the error. I tried different ways, but I can't get the response body incase the HTTP code is 400. With jQuery I used response.responseJSON.html. But this is not available with the fetch api.
So how can I can use the response body as error code.
The fetch API was designed to work best with async functions. If you can make your outer function async, your code would become:
try {
const response = await fetch(url);
if (!response.ok) {
const text = await response.text();
throw Error(text);
}
const jsonResponse = await response.json();
// do whatever you want with the JSON response
} catch (error) {
console.log(error);
}
Otherwise, it gets a bit more complicated:
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
return response.text().then((text) => throw Error(text));
})
.then((jsonResponse) => {
// do whatever you want with the JSON response
}).catch((error) => {
// Handle the error
console.log(error);
});
Related
Hello: i am trying to do a fetch to an endpoint in a react component. The fetch i have is working (all console logs show it is working). since this a function so i can reuse it - i want to return something from it. i tried returning the response but i dont think i have my return in the right place or something else is wrong. here is the log i am referring to:
console.log(response)
and here is the full code:
const doFetch = (fetchUri) => {
fetch(fetchUri)
.then(async response => {
const data = await response.json();
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
console.log('running fetch')
console.log(response)
console.log('showing data from fetch')
console.log(data)
return(response)
// this.setState({ totalReactPackages: data.total })
})
.catch(error => {
this.setState({ errorMessage: error.toString() });
console.error('There was an error!', error);
});
};
So I am hoping someone can help me do that 'return' properly. Thanks! Gerard
you did returned properly inside fetch but you did not returned fetch itself :)
const doFetch = (fetchUri) => {
return fetch(fetchUri)
.then(async response => {
const data = await response.json();
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
console.log('running fetch')
console.log(response)
console.log('showing data from fetch')
console.log(data)
return (response)
// this.setState({ totalReactPackages: data.total })
})
.catch(error => {
this.setState({errorMessage: error.toString()});
console.error('There was an error!', error);
});
};
doFetch('randomurl').then(parsedAndPreparedResponse => this.setState({ data: parsedAndPreparedResponse }))
As Michal pointed out in his answer, you're only returning within the fetch, not from doFetch itself.
But to extend a little
The async/await within the then() is unnecessary since no asynchronous call is being made within it. The callback in then() will be run when the fetch promise is resolved.
So either get rid of the async/await or change your code to only use async/await.
If you do it could look something like this:
const doFetch = async fetchUri => {
try {
const response = await fetch(fetchUri)
// check for error response
if (!response.ok) {
throw new Error({
status: response.status,
statusText: response.statusText
});
}
// On successful call
const data = response.json();
// Return the data
return data();
} catch (error) {
// Error handling
}
};
You can call the function like this:
// Using the await keyword
const response = await doFetch(/* your fetchUri */);
// Using .then()
// The callback function in then() will run when the promise from
// doFetch() is resolved
doFetch(/* your fetchUri */).then(res => {
// Whatever you want to do with the response
});
Remember that in order you use the await keyword you need to be inside an async function.
const anAsyncFunction = async () => {
const response = await doFetch(/* your fetchUri */);
}
I am writing a simple wrapper around fetch.
async function apiCall(
endpoint: string,
{
data,
headers: customHeaders,
...customConfig
}: { data?: Object; headers?: Object } = {}
) {
const config = {
method: data ? 'POST' : 'GET',
body: data ? JSON.stringify(data) : undefined,
headers: {
'content-type': data ? 'application/json' : undefined,
...customHeaders,
},
...customConfig,
}
return fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
const json = await response.json() // 🤔
return json
} else {
// 👇 🚨 what if `response` contains error messages in json format?
return Promise.reject(new Error('Unknown Error'))
}
})
}
it works fine. The problem is with this snippet
return fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
const json = await response.json()
return json
} else {
// 👇 🚨 what if `response` contains error messages in json format?
return Promise.reject(new Error('Unknown Error'))
}
})
if response is not ok, it rejects with a generic Error. This is because by default, window.fetch will only reject a promise if the actual network request failed. But the issue is that, even if response is not ok, it might still be able to have error messages in json format. This depends on the backend implementation details but sometimes you are able to get the error messages in the response body by response.json(). Now this use case is not covered in the wrapper I built.
So I wonder how I am going to be able to account for that? I guess you can do something like
fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
const json = await response.json()
return json
} else {
try {
const json = await response.json()
return Promise.reject(json)
} catch {
return Promise.reject(new Error('Unknown Error'))
}
}
})
but I wonder if there is some more elegant way to do that?
Lastly, I am very aware of libraries like Axios. I built this partly to satisfy my intellectual curiosity.
Btw, a lightly unrelated question but I wonder if these two are equivalent
if (response.ok) {
const json = await response.json()
return json
}
if (response.ok) {
return response.json()
}
Someone flagged my question as a duplicate of this question. In fact they are not the same. I did not make the same assumption as that question did about the API call returning JSON data both on success and on failure. My question is about exactly how we should do in cases where we cannot make such an assumption.
That the response is not ok doesn't prevent you from consuming its body as JSON so your last snippet is indeed how it should be handled.
Now you ask for something "more elegant", well it may not be more elegant but a less redundant way to write the same would be:
fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
return response.json(); // there is no need to await the return value
}
else {
const err_message = await response.json() // either we have a message
.catch( () => new Error( "Unknown Error" ) ); // or we make one
return Promise.reject( err_message );
}
})
And regarding the last question, yes both are equivalent, the await version doing one more round-trip through the microtask-queue and making your code a bit longer, but for all that matters we could say they are the same.
I'd simplify Kaiido even further, fix some bugs, and add a custom error handler.
class ApiCallError extends Error {
response: Response;
body: any;
httpStatus: number;
constructor(response, body) {
super('HTTP error: ' + response.status);
this.httpStatus = response.status;
this.response = response;
this.body = body;
}
}
async function apiCall(endpoint: string, options?: any) {
const config = {
// do your magic here
}
const response = await fetch(endpoint, config);
if (!response.ok) {
throw new ApiCallError(response, await response.json());
}
return response.json();
}
Changes:
You don't need to catch errors if you're just going to throw again.
You don't need .then() if you support await, and this will make your code a lot simpler.
There's really no point in Promise.resolve and Promise.reject, you can just return or throw.
You should not return plain errors, you should throw them (or make sure they are wrapped in a rejecting promise)
Even though in javascript you can 'throw anything' including strings and arbitrary objects, it's better to throw something that is or extends Error, because this will give you a stack trace.
Making all the error information available on the custom error class provides everything a user of apiCall could possibly need.
I tried to check if the status of my request is 200 (OK), but I do not know how to do these things together because the first and second .then, are not "like each other":
function f(path) {
await fetch(path)
.then(response => {
// console.log(response.status);
if (response.status != 200) {
throw response.status;
} else {
// do something
}
})
.then(response => response.json())
.then(...method for the response.json()...)
.catch(error => {
// print some error message
}
}
The second then failed and returned error.
I have a problem when I throw that.
It prints to the error to the console (when I check by changing the path to wrong path and I want to see if I treat errors).
what can I do?
You're checking it correctly in your first fulfillment handler (then callback), although I'd just use !response.ok. You don't usually need the status in the subsequent handlers.
But the problem with your first fulfillment handler is that it's not returning anything, so the subsequent fulfillment handler only sees undefined. Instead, return the promise from json():
function f(path) {
fetch(path)
.then(response => {
if (!response.ok) {
// Note: Strongly recommend using Error for exceptions/rejections
throw new Error("HTTP error " + response.status);
}
return response.json();
})
.then(data => {
// ...use the data here...
})
.catch(error => {
// ...show/handle error here...
});
}
Note that you can't use await in a traditional function, only in an async function. But you don't need it if you're using .then and .catch. I've removed it above.
If for some reason you wanted the status in subsequent fulfillment handlers, you'd have to return it from the first fulfillment handler. For instance:
function f(path) {
fetch(path)
.then(response => {
if (!response.ok) {
// Note: Strongly recommend using Error for exceptions/rejections
throw new Error("HTTP error " + response.status);
}
return response.json().then(data => ({status: response.status, data}));
})
.then(({status, data}) => {
// ...use `status` and `data` here...
})
.catch(error => {
// ...show/handle error here...
});
}
In that, I've used a nested fulfillment handler on the promise from json(), and then returned an object with status and data on it.
You need to return in your then chain, which at a glance appears to be one too many. Check out the following example...
fetch(path)
.then(r => r.ok ? r.json() : Promise.reject('oops')) // .statusText, etc
.then(r => {
// [...]
})
.catch(e => console.error(e)); // oops
a) I don't think you need await keyword since you're using .then() chaining.
b) You have to return something from the first then so as to get that in the next .then()
function f(path) {
await fetch(path)
.then(response => {
// console.log(response.status);
if (response.status != 200) {
throw response.status;
} else {
// do something
// After doing what you need return the response
return response
}
})
.then(response => response.json())
.then(...method for the response.json()...)
.catch(error => {
// print some error message
}
}
Actually, it's not clear what your function has to do. But I think your sturggle comes from not fully understanding how promises chain works. For that, I'd recommend familiarizing yourself with this article, it helped me a lot :)
So back to your function. The elegant solution is adding simple "tap" function, that allows you to do some stuff with current response, but still it passes response further for other .then chains.
Here's the tap function:
const tap = (callback) => (value) => (callback(value), value);
And finally how you can use it:
function f(path) {
fetch(path)
.then(
tap((response) => {
if (response.status !== 200) throw new Error(response.status);
})
)
.then((response) => {
// do other stuff
})
.catch((error) => console.error(error));
}
The number of browsers that support fetch but don't support async/await is now very small, so you may be better off using this simpler syntax first, and then transpiling for legacy browsers along with your shims for fetch.
Your function becomes:
try {
const response = await fetch(path);
// console.log(response.status);
if (response.status != 200) {
throw response.status;
} else {
// do something
}
const parsed = await response.json();
// do something with parsed
}
catch(error) {
// print some error message
}
This new syntax makes it much easier to deal with errors in the different then actions:
const response = await fetch(path);
// console.log(response.status);
if (response.status != 200) {
throw response.status;
} else {
// do something
}
let parsed; // Will hold the parsed JSON
try {
parsed = await response.json();
}
catch(error) {
// Deal with parsing errors
}
try {
// do something with parsed
}
catch(error) {
// Deal with errors using the parsed result
}
Is this the only way to use the body.json() and also get the status code?
let status;
return fetch(url)
.then((response => {
status = response.status;
return response.json()
})
.then(response => {
return {
response: response,
status: status
}
});
This doesn't work as it returns a promise in the response field:
.then((response)=> {return {response: response.json(), status: response.status}})
Your status is not visible in the second then. You can just get the two properties in the single then.
json() returns a new Promise to you, so you need to create your object inside the then of the result of that function. If you return a Promise from a function, it will be fulfilled and will return the result of the fulfillment - in our case the object.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(r => r.json().then(data => ({status: r.status, body: data})))
.then(obj => console.log(obj));
The .json method returns a promise, not the parsed value itself. If you want to access both the response and the parsed value in the same callback, you'll need to use nested functions like this:
fetch(url)
.then(response => {
response.json().then(parsedValue => {
// code that can access both here
})
});
Alternatively, you can use await inside an asynchronous function to eliminate the need for callbacks.
const response = await fetch(url);
const parsedValue = await response.json();
// code that can access both here
Of course, you'll want to check for errors, either with a .catch(...) call on a Promise or with a try...catch block in an async function. You could make a function that handles JSON and error cases, and then reuse it for all fetches. For example, something like this:
function handle(response) {
if (response.ok) {
return response.json().then(parsedValue => {
// the status was ok and the body could be parsed
return Promise.resolve({ response, parsedValue });
}).catch(err => {
// the status was ok but the body was empty or not JSON
return Promise.resolve({ response });
});
} else {
return response.json().catch(err => {
// the status was not ok and the body was unobtainable/empty/not JSON
throw new Error(response.statusText);
}).then(parsedValue => {
// the status was not ok and the body was JSON
throw new Error(parsedValue.error.message); // assuming an error message is returned by our REST API
});
}
}
I don't think it's the best design pattern, but hopefully this clarifies how the fetch API works.
PS: I avoided naming any variable or property json since that is the name of the text format. Once it's been parsed, it is no longer JSON.
Using two 'then's seem unnecessary to me.
async/await could get the job done pretty easily.
fetch('http://test.com/getData')
.then( async (response) => {
// get json response here
let data = await response.json();
if(data.status === 200){
// Process data here
}else{
// Rest of status codes (400,500,303), can be handled here appropriately
}
})
.catch((err) => {
console.log(err);
})
Did you try this?
return fetch(url)
.then((r)=> {return {response: r.json(), status: r.status}})
I think the cleanest way is to create a Promise.all() with the pieces you need.
.then(response => Promise.all([Promise.resolve(response.ok), response.text()]))
Which can be written shorter as
.then(response => Promise.all([response.ok, response.text()]))
The promise returns an array with all of the results
.then(data => ({ status: data[0], response: data[1] }))
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
}
// 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;});
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));
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.
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);
});
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);
}
})
});