Catching promise errors inside another promise's callback - javascript

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.

Related

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

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`

Firebase Functions How To Handle Errors Properly [duplicate]

This question already has an answer here:
Google Cloud Functions - warning Avoid nesting promises promise/no-nesting
(1 answer)
Closed 3 years ago.
NOTE: this question is mainly about error handling, and if this is an ok approach, not about nesting promises, please read before closing
Since there are currently no error codes for services like firestore and firebase database, i'm using a system to know where the function failed and to handle error accordingly, simplified version below:
exports.doStuff = functions.https.onCall((data, context) => {
return [promise doing stuff goes here].catch(error => { throw new Error('ERROR0') })
.then(result => {
return [promise doing stuff goes here, needs result of previous promise]
.catch(error => { throw new Error('ERROR1') })
})
.then(result => {
return [promise doing stuff goes here, needs result of previous promise]
.catch(error => { throw new Error('ERROR2') })
})
.then(result => {
//inform client function successful
return {
success: true
}
})
.catch(error => {
if (error !== null) {
switch (error.message) {
case 'ERROR0':
//do stuff
throw new functions.https.HttpsError('unknown', 'ERROR0');
case 'ERROR1':
//do stuff
throw new functions.https.HttpsError('unknown', 'ERROR1');
case 'ERROR2':
//do stuff
throw new functions.https.HttpsError('unknown', 'ERROR2');
default:
console.error('uncaught error: ', error);
throw error;
}
}
});
});
the thing is, for each .catch() inside each returned promise, i'm getting the following warning: warning Avoid nesting promises
so my question is, is there a better way to handle errors?
Ultimately it's a style recommendation to prevent bizarre and hard to recognise errors. Most of the time a rewrite can eliminate the warning. As an example, you could rewrite your code as the following whilst retaining the same functionality.
exports.doStuff = functions.https.onCall(async (data, context) => {
const result1 = await [promise doing stuff goes here]
.catch(error => {
throw new functions.https.HttpsError('unknown', 'ERROR0', { message: error.message } )
});
const result2 = await [promise based on result1 goes here]
.catch(error => {
throw new functions.https.HttpsError('unknown', 'ERROR1', { message: error.message } )
});
const result3 = await [promise based on result1/result2 goes here]
.catch(error => {
throw new functions.https.HttpsError('unknown', 'ERROR2', { message: error.message } )
});
return {
success: true
};
});
Lastly, rather than using unknown everywhere, you could use one of several possible values for the first argument whilst passing in whatever supporting information you need as the third argument (as shown above where I pass through the original error message).

Is this the correct way of returning a ES6 promise in firebase cloud functions?

I have a cloud function similar to this:
exports.verifyEmail = functions.https.onCall((data, context) => { // data contains session_id (doc id where otp is stored) and otp
return new Promise((resolve, reject) => {
admin.firestore().collection('verification').doc(data.session_id).get().then(doc => {
if(data.otp === doc.data().otp){
return resolve()
} else {
return reject({message: 'OTP did not match'})
}
}).catch(err => {
return reject({message: err.message})
})
})
})
I read this method on a blog somewhere. Now the problem is, when I put wrong OTP on the client side, it shows error as INTERNAL rather than showing the error message OTP did not match. What would be the correct way to send the error message through?
Since the err.message is returning Internal, then you need to change the returned error to what you want:
}).catch(err => {
return reject({message: "OTP did not match"})
})

Unhandled promise rejection - key path incomplete

I keep getting an error from GCP regarding this, I am using datastore & deploying on GAE. Anyone have any ideas why I am getting this error using javascript promises?
I am using a google action to open on google home, ask for an activation keyphrase if the device has not been registered to an apartment number in datastore already. If it is not registered, it asks for a keyphrase that will associate the unique device id with an apartment number. If the unique id has an apartment associated with it, then is asks what it can help with.
I am not sure why it is saying the key path is incomplete. Also I am new to promises! So any help is greatly appreciated
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 99): Error: Key path element must not be incomplete: [Activation: ]
With this code?
datastore.get(datastore.key([ACTIVATION, device_id]))
.then(results => {
let activation = null
if (results[0] ) {
activation = results[0]
}
return Promise.resolve(activation)
})
.then(activation => {
console.log(activation)
let actionMap = new Map();
actionMap.set('input.welcome', assistant => {
console.log('input.welcome')
if (!activation) {
assistant.ask("Hello! May I have your key phrase?")
}
else {assistant.ask("Welcome back, what can I do for you today?")
}
})
actionMap.set('input.unknown', assistant => {
console.log('input.unknown')
if (!activation) {
assistant.ask("Please provide your activation code")
} else
{
let speech = "OK"
if (request.body &&
request.body.result &&
request.body.result.fulfillment &&
request.body.result.fulfillment.messages &&
request.body.result.fulfillment.messages[0] &&
request.body.result.fulfillment.messages[0].speech) {
speech = request.body.result.fulfillment.messages[0].speech
}
sendSMSFromUnit(activation.number, request.body.result.resolvedQuery)
assistant.tell("Got it. ")
}
})
actionMap.set('input.keyphrase', assistant => {
let activationCode = TitleCase([
assistant.getArgument('Token1'),
assistant.getArgument('Token2'),
assistant.getArgument('Token3')
].join(" "))
console.log('activationCode: ' + activationCode)
if (activation && activation.keyphrase == activationCode) {
assistant.tell('This device is activated.')
return
}
datastore.get(datastore.key([APARTMENT, activationCode]))
.then(results => {
console.log(results)
if (!results[0]) {
assistant.ask('Activation unsuccessful. Can you provide your activation code again?')
return
}
let apartment = results[0]
datastore.insert({
key: datastore.key([ACTIVATION, device_id]),
data: {
name: apartment.name,
number: apartment.number,
keyphrase: activationCode,
device_id: device_id
}
}).then(() => {
assistant.ask('Thanks! ')
})
})
})
The whole pattern of a promise is
Promise((resolve, reject) => {
// ...
});
Now how to use it
promiseFunc(...)
.then((x) => {
// It get executed well
})
.catch((x) => {
// An error happened
});
In your code you are missing the .catch part. So if an error get thrown into your promise function you won't catch it and result of a node exception. That's why you have the following warning : Unhandled promise rejection
You are getting that error message because you are not catering for when
the promise rejects rather than resolves.
In your code where you call '.then', that is when the promise has resolved. But you have no action for when the promise is rejected. Take the following example;
// psuedo promise function which resolves if the data is good and rejects if the data is bad
function myPromiseFunction() {
return new Promise((resolve,reject) => {
// do something like make a http call here...
// if the response is good
return resolve(response)
// if the response is not so good
return reject(error)
});
}
// using the promise function
myPromiseFunction()
.then((response) => {
console.log(response);
}, (error) => { // <---- you are missing this part
console.log(error);
});
or you can write it this way
myPromiseFunction()
.then((response) => {
console.log(response);
})
.catch((error) => { // <---- you are missing this part
console.log(error);
})

Break out of Bluebird promise chain in Mongoose

I've studied several related questions & answers and still can't find the solution for what I'm trying to do. I'm using Mongoose with Bluebird for promises.
My promise chain involves 3 parts:
Get user 1 by username
If user 1 was found, get user 2 by username
If both user 1 and user 2 were found, store a new record
If either step 1 or step 2 fail to return a user, I don't want to do step 3. Failing to return a user, however, does not cause a database error, so I need to check for a valid user manually.
I can use Promise.reject() in step 1 and it will skip step 2, but will still execute step 3. Other answers suggest using cancel(), but I can't seem to make that work either.
My code is below. (My function User.findByName() returns a promise.)
var fromU,toU;
User.findByName('robfake').then((doc)=>{
if (doc){
fromU = doc;
return User.findByName('bobbyfake');
} else {
console.log('user1');
return Promise.reject('user1 not found');
}
},(err)=>{
console.log(err);
}).then((doc)=>{
if (doc){
toU = doc;
var record = new LedgerRecord({
transactionDate: Date.now(),
fromUser: fromU,
toUser: toU,
});
return record.save()
} else {
console.log('user2');
return Promise.reject('user2 not found');
}
},(err)=>{
console.log(err);
}).then((doc)=>{
if (doc){
console.log('saved');
} else {
console.log('new record not saved')
}
},(err)=>{
console.log(err);
});
Example
All you need to do is something like this:
let findUserOrFail = name =>
User.findByName(name).then(v => v || Promise.reject('not found'));
Promise.all(['robfake', 'bobbyfake'].map(findUserOrFail)).then(users => {
var record = new LedgerRecord({
transactionDate: Date.now(),
fromUser: users[0],
toUser: users[1],
});
return record.save();
}).then(result => {
// result of successful save
}).catch(err => {
// handle errors - both for users and for save
});
More info
You can create a function:
let findUserOrFail = name =>
User.findByName(name).then(v => v || Promise.reject('not found'));
and then you can use it like you want.
E.g. you can do:
Promise.all([user1, user1].map(findUserOrFail)).then(users => {
// you have both users
}).catch(err => {
// you don't have both users
});
That way will be faster because you don't have to wait for the first user to get the second one - both can be queried in parallel - and you can scale it to more users in the future:
let array = ['array', 'with', '20', 'users'];
Promise.all(array.map(findUserOrFail)).then(users => {
// you have all users
}).catch(err => {
// you don't have all users
});
No need to complicate it more than that.
move your error handling out of the inner chain to the place you want to actual catch/handle it. As i don't have mongo installed, here is some pseudocode that should do the trick:
function findUser1(){
return Promise.resolve({
user: 1
});
}
function findUser2(){
return Promise.resolve({
user: 2
});
}
function createRecord(user1, user2){
return Promise.resolve({
fromUser: user1,
toUser: user2,
});
}
findUser1()
.then(user1 => findUser2()
.then(user2 => createRecord(user1, user2))) // better nest your promises as having variables in your outside scope
.then(record => console.log('record created'))
.catch(err => console.log(err)); // error is passed to here, every then chain until here gets ignored
Try it by changing findUser1 to
return Promise.reject('not found 1');
First, I would recommend using throw x; instead of return Promise.reject(x);, simply for readibility reasons. Second, your error logging functions catch all the errors, that's why your promise chain is continuing. Try rethrowing the errors:
console.log(err);
throw err;
Don't put error logging everywhere without actually handling the error - if you pass an error handler callback you'll get back a promise that will fulfill with undefined, which is not what you can need. Just use
User.findByName('robfake').then(fromUser => {
if (fromUser) {
return User.findByName('bobbyfake').then(toUser => {
if (toUser) {
var record = new LedgerRecord({
transactionDate: Date.now(),
fromUser,
toUser
});
return record.save()
} else {
console.log('user2 not found');
}
});
} else {
console.log('user1 not found');
}
}).then(doc => {
if (doc) {
console.log('saved', doc);
} else {
console.log('saved nothing')
}
}, err => {
console.error("something really bad happened somewhere in the chain", err);
});
This will always log one of the "saved" or "something bad" messages, and possibly one of the "not found" messages before.
You can also use exceptions to achieve this, but it doesn't really get simpler:
var user1 = User.findByName('robfake').then(fromUser => {
if (fromUser)
return fromUser;
else
throw new Error('user1 not found');
});
var user2 = user1.then(() => // omit this if you want them to be searched in parallel
User.findByName('bobbyfake').then(toUser => {
if (toUser)
return toUser;
else
throw new Error('user2 not found');
})
);
Promise.all([user1, user2]).then([fromUser, toUser]) =>
var record = new LedgerRecord({
transactionDate: Date.now(),
fromUser,
toUser
});
return record.save();
}).then(doc => {
if (doc) {
console.log('saved', doc);
} else {
console.log('saved nothing')
}
}, err => {
console.error(err.message);
});

Categories

Resources