Why is `.then()` processed when fetch fails? - javascript

Consider the following code where I tried to shield fetch against any unsuccessful connections (I call them non "200-ish" in the comments) and provide a function that will make use of its successful results:
const callApi = () => {
return fetch("http://doesnotexist.example.com")
.then((r) => {
// check for non200-ish respnses (404, etc.)
if (!r.ok) {
console.log(`status for failed call: ${r.status}`);
throw new Error(`${r.statusText} (${r.status})`);
} else {
// continue the chain because the result is 200-ish
return r;
}
})
.then((r) => r.json())
.catch((err) => {
// should catch network errors (DNS, etc.) as well as replies that are not 200-ish
console.log(`call failed: ${err}`);
});
};
callApi().then((r) => console.log("the call was successful"));
The result is
call failed: TypeError: Failed to fetch
the call was successful
Since this is a network issue, the first then() was not executed and we jumped directly to the catch(). But why has the last then() been executed?
The next example is for a call that returns an error code:
const callApi = () => {
return fetch("https://httpstat.us/500")
.then((r) => {
// check for non200-ish respnses (404, etc.)
if (!r.ok) {
console.log(`status for failed call: ${r.status}`);
throw new Error(`${r.statusText} (${r.status})`);
} else {
// continue the chain because the result is 200-ish
return r;
}
})
.then((r) => r.json())
.catch((err) => {
// should catch network errors (DNS, etc.) as well as replies that are not 200-ish
console.log(`call failed: ${err}`);
});
};
callApi().then((r) => console.log("the call was successful"));
The output is
status for failed call: 500
call failed: Error: Internal Server Error (500)
the call was successful
Same question as above.
Finally, for 200 everything is fine:
const callApi = () => {
return fetch("https://httpstat.us/200")
.then((r) => {
// check for non200-ish respnses (404, etc.)
if (!r.ok) {
console.log(`status for failed call: ${r.status}`);
throw new Error(`${r.statusText} (${r.status})`);
} else {
// continue the chain because the result is 200-ish
return r;
}
})
.catch((err) => {
// should catch network errors (DNS, etc.) as well as replies that are not 200-ish
console.log(`call failed: ${err}`);
});
};
callApi().then((r) => console.log("the call was successful"));
Another way to address the question would be: how to stop processing at the catch()?

You're returning the result of a fetch().then().catch() chain, and calling a .then() on that:
callApi().then((r) => console.log("the call was successful"));
That last .then() will always be executed, because the promise was handled successfully. It either:
Completed successfully, or
catch took care of any errors that occurred`

Related

Within a fetch() Promise, how to .catch server errors messages when status is 4xx or 5xx? [duplicate]

This question already has answers here:
fetch: Reject promise with JSON error object
(5 answers)
Closed last year.
In a locally run Node.js script, this works when status is 200:
// module file
import fetch from "node-fetch";
export const getJSON = () => {
const url = 'https://api.somesite.com/api/v0/etc';
const options = {method: 'GET', headers: {Accept: 'application/json'}};
const request = fetch(url, options)
.then(response => response.json())
.catch(err => console.log("somesite:", err));
return Promise.resolve(request);
};
// execution file
import { getJSON } from './libs/api_requests.mjs';
console.log("func call", await getJSON());
But the fetch also works without triggering the .catch logic when the response status is 4xx or 5xx (see for example this answer).
Execution doesn't break and I actually receive an error message when the function is called as if that would be the correct, normal result - as the output of response.json().
This message is in plain English, something like "error: 'Incorrect path. Please check https://www.somesite.com/api/'".
I would like to preserve/display this error message, only I would like to catch it within the function getJSON in the module file, instead of having to wrap some logic around it at the destination, potentially repeating the same code multiple times everywhere the function is called, instead of dealing with the issue just once at the source.
So I modified the .then clause like this, which also works:
.then(response => { if (response.ok) { // .ok should be status 200 only, I suppose
return response.json();
} else { throw new Error(response.status) }
This now triggers the .catch clause as intended, displaying "Error: 404 [etc]". Except what I would like to throw is the original error message "Incorrect path [etc]" and that I could not do. I tried
.then(response => { if (response.ok) {
return response.json();
} else { throw new Error(response.json()) } // somesite: Error: [object Promise]
.then(response => { if (response.ok) {
return response.json()
} else { throw new Error(Promise.resolve(response.json())) } // somesite: Error: [object Promise]
.then(response => { if (response.ok) {
return response.json()
} else { throw new Error(return response.json()) } // SyntaxError: Unexpected token 'return'
.then(response => { if (response.ok) {
return response.json();
} else { throw new Error(Promise.resolve(request)) } // somesite: Error: [object Promise]
I guess I need to resolve the response.json() promise as if all was ok, but how to do that?
I also had a look at the request object with console.dir(request, { depth: null }) to see if I could extract the error message from there, but I couldn't find it and the object still contained many unexpanded elements like [Function: onerror] or [Function: onclose] for example.
Try response.text() instead of response.json() when the status code is 400 or 500.
In my experience, the error messages are typically returned by the text callback.
See this answer to a similar question.
Edit:
Added the following code, suggested by OP.
.then((response) => {
if (response.ok) {
return response.json();
}
else {
return response.text()
.then((text) => {
throw(text);
// if the error is an object and you just want to display some elements:
throw(JSON.parse(text));
});
}
})
.catch((err) => {
// in case you want to log the error
console.log("somesite: ", err));
return new Error("somesite: " + err);
});

javascript promise handling, fail to handle error

I'm having some trouble understanding what I'm doing wrong. I have a function that receives a url to which should make a GET request, in case of success should fill a combo with the received data (this depends which function calls it), in case of fail it should execute some common code.
getFirstCombo = () => {
this.getFromApi('/First/GetAll')
.then(data => this.setState({firstComboOptions: this.parseCombo(data)}))
.catch(error => console.log('ERROR2: ', error));
}
getSecondCombo = () => {
this.getFromApi('/Second/GetAll')
.then(data => this.setState({secondComboOptions: this.parseCombo(data)}))
.catch(error => console.log('ERROR2: ', error));
}
parseCombo = (data: any) => {
const combo = data.map(item => (
{ label: item.description, value: item.id }
));
return combo;
}
getFromApi = (url: string) : Promise<any> => {
return restApiAxios.get(url)
.then(response => {
return response.data;
})
.catch(error => {
console.log('ERROR: ', error);
});
}
this code is executed on the componentDidMount of the react component, but when it fails, it first prints :
ERROR: Error: Network Error
at createError (createError.js:16)
at XMLHttpRequest.handleError (xhr.js:83)
and immediately after:
PanelDatos.tsx:50 ERROR2: TypeError: Cannot read property 'map' of undefined
at PanelDatos.parseCombo (PanelDatos.tsx:55)
at PanelDatos.tsx:50
so, when failing executes the catch block from getFromApi and then it tries to execute the then block in getFirstCombo, which triggers the catch block from the same function cause data does not exist, why is that? shouldnt it just execute the first catch?
thanks in advance
.catch returns a promise much like .then, allowing you to return a custom value and handle it that way.
Try doing the following to observe the effect:
Promise
.reject(1)
.catch(e => e) // Catch the error and return it
.then(console.log) // will log 1 to the console
This means you'll need to add some checks if you want to continue to use promises like this:
Promise
.reject(new Error('haha'))
.catch(err => ({err}))
.then(({err, data}) => {
if(err) return // Do nothing
// enter code here
})
However, using async / await will improve readability even more:
getFirstCombo = async () => {
let response
try {
response = await this.getFromApi('/First/GetAll')
} catch (e) {
return // Exit early
}
let parsed
try {
parsed = this.parseCombo(data)
} catch (e) {
console.log(e)
return // Exit early
}
return this.setState({firstComboOptions: parsed})
}
And, of course, throw the error again in your catch block in your api to allow it to handle api calls.
This is happening since inside getFromApi catch method on the error you are not returning anything, so by default, it is returning a resolved promise with null response and the execution goes inside getFirstCombo then method, causing another error. You can update your code to resolve this like:
getFromApi = (url: string): Promise<any> => {
return restApiAxios.get(url)
.then(response => response.data)
.catch(error => Promise.reject(error));
}
The static Promise.reject function returns a Promise that is rejected. So, it will go directly into catch of wherever getFromApi is called.
DEMO:
async function getFromApi(url) {
return fetch(url) // rejects
.then(response => response.json())
.catch(err => Promise.reject(err))
}
async function getFirstCombo() {
getFromApi('https://no-such-server.abcd')
.then(data => console.log('data: ', data))
.catch(error => console.log('ERROR2: ', error));
}
getFirstCombo()
DEMO #2 (With getFirstCombo function not having any catch block) :
async function getFromApi(url) {
return fetch(url) // rejects
.then(response => response.json())
.catch(err => {
console.log('ERROR in getFromApi(): ', err);
return null; // return null, empty array, 0 or false... as per your requirement
})
}
async function getFirstCombo() {
getFromApi('https://no-such-server.abcd')
.then(data => console.log('data: ', data))
// Same value set in catch block of getFromApi will return in this then() block
// Validate this `data` variable before processing it further like:
// if(data === null) this means an error had occurred
// else continue with your logic
}
getFirstCombo()

Catching promise errors inside another promise's callback

The code below runs as expected. If the charge function is invoked, the function fetches the relevant ticket object from firestore and then returns it back to the client.
If the ticket doesn't exist, the function throws a HttpsError with an error message that will be parsed by the client.
exports.charge = functions.https.onCall(data => {
return admin.firestore().collection('tickets').doc(data.ticketId.toString()).get()
.then((snapshot) => {
return { ticket: snapshot.data() }
})
.catch((err) => {
throw new functions.https.HttpsError(
'not-found', // code
'The ticket wasn\'t found in the database'
);
});
});
The problem comes after this. I now need to charge the user using Stripe, which is another asynchronous process that will return a Promise. The charge requires the pricing info obtained by the first async method, so this needs to be called after snapshot is retrieved.
exports.charge = functions.https.onCall(data => {
return admin.firestore().collection('tickets').doc(data.ticketId.toString()).get()
.then((snapshot) => {
return stripe.charges.create(charge) // have removed this variable as irrelevant for question
.then(() => {
return { success: true };
})
.catch(() => {
throw new functions.https.HttpsError(
'aborted', // code
'The charge failed'
);
})
})
.catch(() => {
throw new functions.https.HttpsError(
'not-found', // code
'The ticket wasn\'t found in the database'
);
});
});
My problem is with catching errors in the new charge request. It seems that if the charge fails, it successfully calls the first 'aborted' catch, but then it is passed to parent catch, and the error is overridden and the app sees the 'ticket not found' error.
How can I stop this from happening? I need to catch both errors separately and throw a HttpsError for each one.
Generally, such problems are can be handled with adding status node and then chaining with a final then block. You can try something like following
exports.charge = functions.https.onCall(data => {
return admin.firestore().collection('tickets').doc(data.ticketId.toString()).get()
.then((snapshot) => {
return stripe.charges.create(charge)
.then(() => {
return { success: true };
})
.catch(() => {
return {
status : 'error',
error : new functions.https.HttpsError(
'aborted', // code
'The charge failed',
{ message: 'There was a problem trying to charge your card. You have NOT been charged.' }
)};
})
})
.catch(() => {
return {
status : 'error',
error : new functions.https.HttpsError(
'not-found', // code
'The ticket wasn\'t found in the database',
{ message: 'There was a problem finding that ticket in our database. Please contact support if this problem persists. You have NOT been charged.' }
)};
}).then((response) => {
if(response.status === 'error') throw response.error;
else return response;
});
});
Don't nest a then inside another then for multiple items of work:
work1
.then((work1_results) => {
return work2.then((work2_results) => {
// this is bad
})
})
Instead, perform all your work as a chained sequence:
work1
.then((work1_results) => {
return work2
})
.then((work2_results) => {
// handle the results of work2 here
})
You can store intermediate results in higher-scoped variables if you need to accumulate data between your callbacks.

Catching error in fetch() function

I recently have learned something about fetch() and promise, and now I need to use it in project. Here I have a fetch() function, which works very well, but I think, it must catch an error. So, what is the best way to catch error in fetch() functions? And i need to catch them in both then()?
Here some code:
const endpoint = 'http://localhost:3030/api/hotels';
const promise = fetch(endpoint)
.then(res => res.json(), err => {
console.log(err);
})
.then(parseRooms, err => {
console.log(err);
})
Thank you !
Use the fact that promise handlers chain together. Each call to then or catch creates a new promise, which is chained to the previous one.
So in your case:
const promise = fetch(endpoint)
.then(res => res.json())
.then(parseRooms)
.catch(error => {
// Do something useful with the error
});
I'm assuming there that parseRooms throws an error if there's a problem with the structure it receives.
You probably want to check res.ok in there, too, since fetch only fails if there was a network error, not if there was an HTTP error such as a 404:
const promise = fetch(endpoint)
.then(res => {
if (!res.ok) {
throw new Error(); // Will take you to the `catch` below
}
return res.json();
})
.then(parseRooms)
.catch(error => {
// Do something useful with the error
});

How do I fix a race condition in a nested Promise in Node.js? [duplicate]

I'm creating an API using Node.js/TypeScript running Express. Below is an excerpt from my get method. An error is being triggered in the format method, which throws an error that is caught by the promise, but not propagated to the parent promise after a throw:
this.getModel(objectName).findAll(queryParameters).then(function(databaseObjects) {
for (let databaseObject of databaseObjects) {
var jsonObject = {};
//console.log("Database object: ");
//console.log(databaseObject);
transform.baseFormat(databaseObject, jsonObject)
.then(() => transform.format(databaseObject, jsonObject))
.then(() => {
res.locals.retval.addData(jsonObject);
}).catch((e) => {
console.log("Caught error during format of existing object: ");
console.log(e);
throw e;
});
}
})
.then(() => {
if (metadata) {
this.metadata(objectName, false, transform, res.locals.retval);
delete queryParameters.limit;
delete queryParameters.offset;
console.log("RUNNING METADATA COUNT: ");
this.getModel(objectName).count(queryParameters).then(function(count) {
res.locals.retval.setMetadata("records", count);
return next();
}).catch(function(e) {
this.error(e, res);
return next();
});
} else {
console.log("NO METADATA");
return next();
}
})
.catch((e) => {
// TODO: Move status into error() function
console.log("500 Error on GET");
console.error(e);
res.locals.retval.addError(ErrorCode.InternalError, e);
res.status(ErrorCode.InternalError).send(res.locals.retval);
return next();
});
Here's the output:
(node:8277) Warning: a promise was created in a handler at /Library/WebServer/adstudio/dist/server.js:555:51 but was not returned from it, see
at Function.Promise.bind (/Library/WebServer/adstudio/node_modules/bluebird/js/release/bind.js:65:20)
Caught error during format of existing object:
Test Error
END FUNCTION HAS BEEN REACHED!
Then the request fails to finish.
I've read a lot on Promises and I haven't been able to find an issue/solution similar to mine.
http://bluebirdjs.com/docs/warning-explanations.html
http://taoofcode.net/promise-anti-patterns/
https://www.reddit.com/r/javascript/comments/4bj6sm/am_i_wrong_to_be_annoyed_with_promise_error/
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
Chained promises not passing on rejection
http://wiki.commonjs.org/wiki/Promises/A
https://promisesaplus.com/
Running inside that for-loop is not asynchronous, so your promise is resolving basically as soon as the loop finishes, yet before all your formatting finishes.
Use a promise control flow, like bluebird's Promise.each which is serial or just Promise.all. Then any exceptions will be caught.
this.getModel(objectName).findAll(queryParameters).then(function (databaseObjects) {
var promises = databaseObjects.map(databaseObject => {
var jsonObject = {}
// console.log("Database object: ");
// console.log(databaseObject);
return transform.baseFormat(databaseObject, jsonObject)
.then(() => transform.format(databaseObject, jsonObject))
.then(() => {
res.locals.retval.addData(jsonObject)
}).catch((e) => {
console.log('Caught error during format of existing object: ')
console.log(e)
throw e
})
})
return Promise.all(promises)
})
.catch((e) => {
// TODO: Move status into error() function
console.log('500 Error on GET')
console.error(e)
res.locals.retval.addError(ErrorCode.InternalError, e)
res.status(ErrorCode.InternalError).send(res.locals.retval)
return next()
})

Categories

Resources