The following code is taken from the last task of https://javascript.info/promise-api.
When I run the following, I am unable to get the output to match the alerts that the comments indicate. I assume that I am missing somethign with the catch statements, but I do not understand where I am going wrong. I appreciate any help!
// the whole promise chain fails with an error here
// change that:
// make errors appear as members of the results array
let urls = [
'https://api.github.com/users/iliakan',
// this URL is HTML page, it's invalid JSON, so response.json() fails
'/',
// this URL is invalid, so fetch fails
'http://no-such-url'
];
// Fix it:
Promise.all(urls.map(url => fetch(url).catch(err=>err)))
.then(responses => Promise.all(
responses.map(r => r.json().catch(err=>err))
))
// Demo output (no need to change):
.then(results => {
alert(results[0].name); // Ilya Kantor
alert(results[1]); // SyntaxError: Unexpected token < in JSON at position 0
alert(results[2]); // TypeError: failed to fetch (text may vary)
});
You do get an error from your code. In Firefox, for example, it will say TypeError: r.json is not a function in the developer console. (I see you're using alert() so you might not be familiar with the developer console and console.log() available in browsers. If this is so, I'd suggest looking in to them as the information they provide can be invaluable.)
The problem is that, in r.json(), r is either a response object or an exception object due to the earlier, first .catch(err=>err). Since exception objects do not have a json property, it throws its own exception. That exception isn't caught because there's no try/catch for it and .catch() is only useable on promises.
You could do something like this to check for and pass along an initial exception:
responses.map(r => r.json ? r.json().catch(err=>err) : r)
The reason that this doesn't work is because in the first .catch(err=>err) statement, it is treating errors like a standard (successful) result. Then, any faulty data from fetch is called into the next Promise.all statement as it is treated as a good result, and thus r.json() will not know what to do with any faulty data (which is from fetch('/').
Related
I have a simple Web app running with NodeJS and Express. It has a route where an outside 3rd party can POST us a XML document, which we then convert to JSON then save to our MongoDB database. A few things can go wrong:
The XML can be malformed
The request might be empty
The outside 3rd party might send us duplicate documents
Rather than having an endless series of then() blocks, going deeper and deeper, indented further and further, I wanted to throw an exception for each possible error, and then catch those errors at the top level and process them there.
So we find a unique id and then check to see if this unique id is already in MongoDB:
// will throw an error if there is a duplicate
document_is_redundant(AMS_945, unique_id);
The function looks like this:
function document_is_redundant(this_model, this_unique_id) {
return this_model.findOne({ unique_id : this_unique_id })
.exec()
.then((found_document) => {
// 2021-11-28 -- if we find a duplicate, we throw an error and handle it at the end
// But remember, we want to return a HTTP status code 200 to AMS, so they will stop
// re-sending this XML document.
if (found_document != 'null') {
throw new DocumentIsRedundantException(this_unique_id);
}
});
// no catch() block because we want the exception to go to the top level
}
This gives me: UnhandledPromiseRejectionWarning
Maybe I'm thinking too much like Java instead of Javascript, but I was assuming that if I didn't catch() the exception in that function, it would bubble up to the top level, which is where I want to deal with it. Also assumed it would interrupt the flow of the code at the line where I call the function.
Sadly, the uncaught exception does not interrupt the main thread of execution, so the document is saved, even when it is a duplicate.
So I'm left thinking the only way I can make this work is to return the Promise from the function and then have a then() block after the call to the document_is_duplicate function?
I dislike having to nest then() blocks inside of then() blocks, several levels deep. This seems like bad code. Is there another way?
Not sure why you want to throw an error if your document exists. Look for it, Mongoose will return a document if it exists, or null if it does not. Then simply await the result. Mongoose methods can be awaited, and if you add .exec() they even return a true Promise, which makes your life even easier :
const document_is_redundant = (this_model, unique_id) => this_model.findOne({ unique_id }).lean().exec();
// Now you use it this way
if( !(await document_is_redundant(AMS_945, unique_id))){ // If the returned value is not null
console.log("Document is redundant! Aborting")
return;
}
// Returned value was null
console.log("The document doesn't exist yet!")
Is there a substantial difference between throwing vs returning an error from a service class?
class ProductService {
async getProduct(id) {
const product = await db.query(`...`)
if (!product) {
throw new ProductNotFoundError()
}
return product
}
// VS
async getProduct(id) {
const product = await db.query(`...`)
if (!product) {
return {
data: null,
error: new ProductNotFoundError()
}
}
return {
error: null,
data: product
}
}
}
In this example, I'd like to raise an error message if a product is not found ( supposedly returning null is not enough, maybe I need to also provide additional info why the product is not found ).
As a general design principle, is there a preferred approach, are there any substantial pros/cons of both approaches?
I've been doing the throw new Error example, but then the code gets riddled with try/catch or .then/.catch statements, which make it somewhat harder to reason about.
What I don't particularly like about throwing errors is.. they are not unexpected. Obviously, I expect that a product might not be found, so I added a piece of logic to handle that part. Yet, it ends up being received by the caller the same way, for example, a TypeError is received. One is an expected domain error, the other one is unexpected exception.
In the first scenario, if I return an object with data and error properties ( or I can return an object with methods like isError, isSuccess and getData from each service, which is similar ), I can at least trust my code to not raise exceptions. If one happens to arise, then it will be by definition unexpected and caught by the global error handlers ( express middleware, for example ).
Is there a substantial difference between throwing vs returning an error from a service class?
Big difference. throw in an async function causes the promise that is returned from any async function to be rejected with that exception as the reject reason. Returning a value becomes the resolved value of the promise.
So, the big difference is whether the promise returns from the async function is resolved with a value or rejected with a reason.
As a general design principle, is there a preferred approach, are there any substantial pros/cons of both approaches?
Most people use promise rejections for unusual conditions where the caller would typically want to stop their normal code flow and pursue a different code path and use return values for something that is likely to continue the normal code flow. Think about chained promises:
x().then(...).then(...).catch(...)
If the condition should go immediately to the .catch() because something serious is busted, then a throw/rejection makes the most sense in x(). But, if the condition is just something to typically handle and continue the normal execution, then you would want to just return a value and not throw/reject.
So, you try to design x() to be the easiest for the expected use. A product not being found in the database is an expected possible condition, but it also might be something that the caller would want to abort the logic flow they were in, but the treatment of that condition will certainly be different than an actual error in the database.
As a reference example, you will notice that most databases don't treat a failure to find something in a query as an error. It's just a result of "not found".
Attempting to use axios to make a get request at the following endpoint, and I keep getting errors:
When I check it using Postman and in a browser (GET request), it returns data just fine, but otherwise I can’t get a response.
This is the call I’m using, I don’t know if it’s some sort of issue with my code or with axios itself:
axios.get(`https://api.scryfall.com/cards/named?exact=${args.name}`)
.then((res) => {
console.log(JSON.stringify(res));
})
.catch((err) => {
if (err.response) {
throw new Error(`Card with name (${name}) not found!`)
}
throw new Error(`Could not complete that query!`)
})
The argument args.name is passed as part of a GraphQL resolver, and it definitely has a value, so not sure what the deal is.
Any help would be greatly appreciated!
There are a couple of problems here.
Generally it's not a good idea to throw new errors after catching the original error. As written, your code throws an additional error because the axios Promise is thrown again instead of being dealt with inside catch - so node will complain that you didn't resolve or reject the promise.
The substantive issue is the same as the one answered here except for res - the actual error is TypeError: Converting circular structure to JSON which is caused by trying to JSON.stringify the res object:
JSON doesn't accept circular objects - objects which reference themselves. JSON.stringify() will throw an error if it comes across one of these.
The request (req) object is circular by nature - Node does that.
In this case, because you just need to log it to the console, you can use the console's native stringifying and avoid using JSON
So you can fix this by changing your code to:
axios.get(`https://api.scryfall.com/cards/named?exact=${args.name}`)
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
if (err.response) {
console.error(`Card with name (${name}) not found!`)
} else {
console.error(`Could not complete that query!`)
}
})
If you were just using console.log for testing/as an example and actually need to stringify the data to use some other way, just make sure you're stringifying the data (which is presumably what you actually want) and not the whole res object:
axios.get(`https://api.scryfall.com/cards/named?exact=${args.name}`)
.then((res) => {
let scryfallData = JSON.stringify(res.data)
doSomethingWith(scryfallData)
})
i am working on a ToDo list and its basically done. but i am getting this error in the console that i haven't come across yet, its preventing me to create the list (to do list)
This is the error im getting:
OPTIONS http://localhost:4000/cpds/add
net::ERR_NAME_NOT_RESOLVED
Uncaught (in promise) Error: Network Error createError.js:17
at createError (createError.js.17)
at XMLHttpRequest.handelError (xhr.js:80)
Can someone please explain what this means and how to resolve this issue.
the list prints in my console but not in my browser, then prints this error afterwards.
ERR_NAME_NOT_RESOLVED - points that system fail to resolve IP address for given hostname (http://localhost:4000/cpds/add in your case). While it is very unlikely that you are realy could not resolve address for localhost itself most probable reason is that you requesting for closed port (:4000).
In general this message say Uncaught which means that somewhere in you code when you request for "http://localhost:4000/cpds/add" form axios (it is assumtion cause you don't gave any details about your code) you have statement like
axios.get(url, { headers })
.then(data => console.log(data))
without
.catch(error => console.error(error))
so full version is
axios.get(url, { headers })
.then(data => console.log(data))
.catch(error => console.error(error))
So when request is fails due to any reason (probably error in url in you case) interpreter don't know how to overcome it (other words you should directly define function which would be called in case of error and pass it to catch method).
To ensure error is in url try to place http://localhost:4000/cpds/add to address bar of you browser, if it is realy unaccessable, browser should show you an error.
This is because one of your calls returned a rejected promise/async function, or in other words: An error that occurred calling your function.
Be careful about this. You can write yourlibrarycall.then(result => ...).catch(error => ...) But this can quickly get a pitfall. The catch clause will be called if the library call failed, but also when the .then clause failed. You'd expect the failure came from the library call, but this was fine, your code might also had a problem and the value that the variable error returns might be totally different (or undefined).
Hence i prefer having:
yourFunction = async () => {
let result;
try {
result = await yourlibrarycall // this is blocking
}
catch (error) {
// error handling only of your library call
}
// here comes your following logic
...
}
Using asnyc, your function is executed asynchronously and can now wait for the result using the keyword await. If the library call failed, it will enter the catch scope and provide you a variable with the error occurred.
This is now all the error handling and only will now only cope with the request, the following logic is then executed afterwards, getting rid of the misleading .then(...).catch(...).
If you still want to use the promise approach instead of async/await be careful to handle all the errors in the catch clause explicitly, otherwise they'll bubble up and will be catched by the catch clause, as stated above.
With promises, we can use a variant of .then to split up the chain when an error occurs. Here is an example using fetch
fetch('http://website.com').then(
// Perform some logic
(response) => response.json().then(({ answer }) => `Your answer: ${answer}`),
// Skip json parsing when an error occurs
(error) => 'An error occurred :(',
).then(console.log);
This allows me to skip the response processing logic and only respond to errors that were raised in the original fetch statement. Something similar in RxJS might look like this:
Observable.fromPromise(fetch('http://website.com'))
// if I put .catch here, the result will be piped into flatMap and map
.flatMap(response => response.json())
.map(({ answer }) => `Your answer: ${answer}`)
// if I put .catch here, errors thrown in flatMap and map will also be caught
.subscribe(console.log);
As the comments in the code state, I can't simply put a catch operator in as it doesn't have the same behaviour as my promise chain.
I know I can get it with custom operators involving materialising, or merging an error catching observable with this one, but it all seems like pretty major overkill. Is there a simple way to achieve the promise chain behaviour?
Actually, if I was in your situation I wouldn't be worried about catching errors from flatMap and map. When the source Observable throws an error then it'll be propagated to the observer. So I'd just use an error handler when calling subscribe (otherwise the error is rethrown):
.subscribe(console.log, err => console.log('error:', err));
Note that when an error occurs in the source Observable (a Promise in your case) that it's propagated as error notifications, not as standard next notifications. This means that flatMap() and map() won't have absolutely any effect on the error message. If you used catch() or materialize() that both operators (flatMap and map) would have to be able to handle this type of data (and not throw yet another error).
Anyway you can always use share() or publish() and make two different subscriptions where each handles only one type of signals:
let source = Observable.fromPromise(fetch('http://website.com')).publish();
source
.subscribe(undefined, err => console.log(err));
source
.flatMap(...)
.map(...)
.subscribe(console.log, () => {});
source.connect();
Now I have a separate observer only for errors.
Note that I had to make an empty callback with () => {} so the error will be silently ignored. Also note that when using multicasting (the publish() operator) then the Subject inside might have some specific behavior I should be aware of but maybe it doesn't matter in your use-case.