I am trying to get used to using Promises.
However, I am struggling to get an error throw in a sub-function to bubble up to the main function.
I use Senecajs for all my modules, like so:
// resend-invite.js
module.exports = function(options) {
const Promise = require('bluebird'),
seneca = this,
act = Promise.promisify(seneca.act, {context: seneca});
this.add({role:'project-actions', cmd: 'resend-invite'}, function( msg, done ) {
return findProject()
.then(function(project) {
if ( !project.userHasAdminAccess( msg.user_id ) ) {
throw Error( 'User does not have admin access.' );
}
// makes it here...
return act( { role: "user-repository", cmd: "get-referral-user", user: { email: msg.email } } );
})
.then(function(referral_user) {
// doesn't make it here :(
var referral_name = referral_user.first_name + " " + referral_user.last_name;
return anotherFunction();
})
.then(function(mail_info) {
done(null, mail_info);
})
.catch(function(err) {
// doesn't make it here :(
done(err);
})
});
}
And like this:
// get-referral-user.js
module.exports = function(options) {
const Promise = require('bluebird'),
seneca = this,
User = require('app/models/user');
this.add({role:'user-repository', cmd: 'get-referral-user'}, function( msg, done ) {
return User.findOne(msg.user).exec()
.then(function(user) {
var referral_user_id;
if (user == null) {
throw Error("User wasn't found.");
}
referral_user_id = user.referral_user_id;
return User.findById(referral_user_id).exec()
})
.then(function(user) {
done(null, user.toObject());
})
.catch(function(err) {
// gets to this err handler
done(err);
})
});
}
However, when there is an error in get-referral-user.js, it is caught by the get-referral-user.js .catch block, but the error is not caught by thr resend-invite.js catch block.
How do I get the resend-invite.js code to detect that a sub-function (act( { role: "user-repository", cmd: "get-referral-user", user: { email: msg.email } } ) in this case) has an error and needs to be handled.
Here are the parts of the error that is produced in my console that could be helpful:
Seneca Fatal Error
Message: seneca: Action cmd:get-referral-user,role:user-repository failed: User wasn't found..
..
ALL ERRORS FATAL: action called with argument fatal$:true (probably a plugin init error, or using a plugin seneca instance)
Any help is greatly appreciated. Thank you.
Related
I can't catch an exception thrown by the service worker's message event..
The client uses following code to execute the command on the SW:
import { messageSW } from "workbox-window";
// .. code for Workbox initialization/registration omitted
messageSW(registration?.active, { type: "SYNC" })
.then((results) => {
console.log("done");
})
.catch((e) => {
console.error(e);
});
On the SW (sw.js) side I have the following code:
self.addEventListener("message", async (event) => {
if (requestType === "SYNC") {
event.ports[0].postMessage(await longRunningTask());
}
});
This solution works OK as long as the SW is not throwing any exceptions. Meaning that the client prints the "done" message after the long running process on the SW is executed. If the exception is thrown nothing gets returned, ever.
I have managed to fix the problem by doing the following:
self.addEventListener("message", async (event) => {
if (requestType === "SYNC") {
try {
event.ports[0].postMessage(await longRunningTask());
} catch (error) {
event.ports[0].postMessage(error);
}
}
});
In this case - the result is always returned regardless, "done" is printed, but:
how do I actually produce an exception from the service worker, so the client could catch and handle it?
In general it would be good to hear if what I am doing is an appropriate approach to how asynchronous code on the SW shall be invoked from the client...
Here is my own solution I ended up using:
On service worker side - helper method:
async function replyToSenderAsync(event, task) {
let isCanReply = event.ports && event.ports.length >= 0;
try {
const result = await task();
if (isCanReply) {
event.ports[0].postMessage({ error: null, message: result });
}
} catch (error) {
if (isCanReply) {
event.ports[0].postMessage({ error: error, message: null });
}
}
}
When exception is caught we set the error property. Use as:
self.addEventListener("message", async (event) => {
const requestType = event?.data?.type;
if (requestType === "QUEUE_CLEAR") {
await replyToSenderAsync(event, async () => await clearQueueAsync());
}
});
On client side request wrapper:
function sendMessageToSWAsync(targetSW, messageType, message) {
return new Promise(function (resolve, reject) {
if (
!isServiceWorkerSupported.value ||
!isServiceWorkerRegistered.value ||
!targetSW
) {
reject(new Error("Unable to send the message to a service worker"));
}
try {
messageSW(targetSW, { type: messageType, message: message })
.then((messageResponse) => {
if (!messageResponse) {
reject(new Error("Service worker responsed with empty response"));
} else {
if (messageResponse.error) {
reject(messageResponse.error);
} else {
resolve(messageResponse.message);
}
}
})
.catch((messageError) => {
reject(messageError);
});
} catch (error) {
reject(error);
}
});
}
The magic here is to read the error property and reject the promise if that is the case (hence causing an exception to be thrown). Use as
try {
let response = await sendMessageToSWAsync(registration?.active, "QUEUE_GET_ALL");
}
catch(error) {
}
sendMessageToSWAsync(registration?.active, "QUEUE_GET_ALL")
.then((response) => {})
.catch((error) => {})
I am writing an application in Express.js with a separate controller layer and a service layer. Here is my current code:
user.service.js
exports.registerUser = async function (email, password) {
const hash = await bcrypt.hash(password, 10);
const countUser = await User.countDocuments({email: email});
if(countUser > 0) {
throw ({ status: 409, code: 'USER_ALREADY_EXISTS', message: 'This e-mail address is already taken.' });
}
const user = new User({
email: email,
password: hash
});
return await user.save();
};
exports.loginUser = async function (email, password) {
const user = await User.findOne({ email: email });
const countUser = await User.countDocuments({email: email});
if(countUser === 0) {
throw ({ status: 404, code: 'USER_NOT_EXISTS', message: 'E-mail address does not exist.' });
}
const validPassword = await bcrypt.compare(password, user.password);
if (validPassword) {
const token = jwt.sign({ email: user.email, userId: user._id }, process.env.JWT_KEY, { expiresIn: "10s" });
return {
token: token,
expiresIn: 3600,
userId: user._id
}
} else {
throw ({ status: 401, code: 'LOGIN_INVALID', message: 'Invalid authentication credentials.' });
}
};
user.controller.js
exports.userRegister = async function (req, res, next) {
try {
const user = await UserService.registerUser(req.body.email, req.body.password);
res.status(201).json({ data: user });
} catch (e) {
if(!e.status) {
res.status(500).json( { error: { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred.' } });
} else {
res.status(e.status).json( { error: { code: e.code, message: e.message } });
}
}
}
exports.userLogin = async function (req, res, next) {
try {
const user = await UserService.loginUser(req.body.email, req.body.password);
res.status(200).json({ data: user });
} catch (e) {
if(!e.status) {
res.status(500).json( { error: { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred.' } });
} else {
res.status(e.status).json( { error: { code: e.code, message: e.message } });
}
}
}
The code works, but requires some corrections. I have a problem with error handling. I want to handle only some errors. If another error has occurred, the 500 Internal Server Error will be returned.
1) Can I use "throw" object from the service layer? Is this a good practice?
2) How to avoid duplication of this code in each controller:
if(!e.status) {
res.status(500).json( { error: { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred.' } });
} else {
res.status(e.status).json( { error: { code: e.code, message: e.message } });
}
3) Does the code require other corrections? I'm just learning Node.js and I want to write the rest of the application well.
Yes, you can throw errors from service layer, it is good practice to catch errors with try/catch block in controller
I handle this with a custom error middleware, just use a next function in a catch block.
catch (e) {
next(e)
}
Example of error middleware (for more info check docs, fill free to move a middleware to file)
app.use(function (err, req, res, next) {
// err is error from next(e) function
// you can do all error processing here, logging, parsing error messages, etc...
res.status(500).send('Something broke!')
})
From my point of view it looks good. If you looking for some best practice and tools, try eslint (with AirBnb config for example) for linting, dotenv for a environment variables management, also check Node.js Best Practice
i want to give you an example:
this code in your controller
findCar(idCar)
} catch (error) {
switch (error.message) {
case ErrorConstants.ELEMENT_NOT_FOUND('LISTING'): {
return {
response: {
message: ErrorMessages.ELEMENT_NOT_FOUND_MESSAGE('LISTING'),
},
statusCode,
}
}
default: {
return {
response: {
message: ErrorMessages.UNKNOWN_ERROR_MESSAGE,
},
statusCode,
}
}
}
}
and this code in your service
findCar: async listingId => {
try {
if (some condition) {
throw new Error(ErrorConstants.ELEMENT_NOT_FOUND('LISTING'))
}
return { ... }
} catch (error) {
console.error(error.message)
throw new Error(ErrorConstants.UNKNOWN_ERROR)
}
},
controller is going to catch the service's errors
I'm very new to node.js so I think I'm missing something obvious here.
I'm simply trying to get a list of SQS queues using aws-sdk and return them from a module to be accessible to other code. list_queues is the function in question.
The code below works to an extent, I see a "success" log and a log of a string array of all my queues, however, the function does not return that array to the caller and I don't understand why.
const AWS = require('aws-sdk');
AWS.config.update({region: 'eu-west-1'});
var sqs;
var sts = new AWS.STS();
sts.assumeRole({
RoleArn: 'arn:aws:iam::xxxxx:role/UserRole',
RoleSessionName: 'NodeDeveloperRoleSession'
}, function(err, data) {
if (err) { // an error occurred
console.log('Cannot assume role :(');
console.log(err, err.stack);
} else { // successful response
console.log('Assumed role success :)');
AWS.config.update({
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretAccessKey,
sessionToken: data.Credentials.SessionToken
});
sqs = new AWS.SQS({apiVersion: '2012-11-05'});
}
});
exports.list_queues = function() {
sqs.listQueues({}, function(err, data) {
if (err) {
console.log("Error", err);
} else {
console.log("success");
console.log(data.QueueUrls);
return data.QueueUrls;
}
});
}
Any help is appreciated
exports.list_queues = function() { // 2. but you actually want to return from this one
sqs.listQueues({}, function(err, data) { <-----------------
if (err) { |
console.log("Error", err); |
} else { |
console.log("success"); |
console.log(data.QueueUrls); |
return data.QueueUrls; // 1. you are returning from this one
}
});
}
there are two ways you can make it work
Promise based
exports.list_queues = function() {
return sqs.listQueues({}).promise().then((data) => data.QueueUrls);
}
// and in another file you would:
const {list_queues} = require('./list_queues.js');
list_queues.then((queues) => console.log(queues));
Callback based
exports.list_queues = function(cb) { // notice I added callback here
sqs.listQueues({}, function(err, data) {
if (err) {
console.log("Error", err);
} else {
console.log("success");
console.log(data.QueueUrls);
cb(data.QueueUrls);
}
});
}
// and in another file you would:
const {list_queues} = require('./list_queues.js');
list_queues(function(queues) {
console.log(queues);
});
I strongly recommend you to use promise based approach, since it's much more readable and you can make use of async/await on it, which is great.
Consider this snippet
fetch(`http://${api.host}:${api.port}/user`)
.then(function(data) {
return data.json();
}, function(err) {
throw new Error(`Couldn\'t fetch user data from server: ${err.message}`);
}).then(function(eparkUser) {
for (var key in eparkUser) {
if (eparkUser.hasOwnProperty(key) && !user.hasOwnProperty(key)) {
user[key] = eparkUser[key];
}
}
done(null, user);
}, function(err) {
throw new Error(`Couldn't parse returned json: ${err.message}`);
}).catch(function(e) {
done(e);
});
Isn't throw supposed to break the chain and trigger .catch ? How to achieve this behaviour? Becauce now both throw are getting executed and I see message:
Error: Couldn't parse returned json: Couldn't fetch user data from server: request to http://localhost:3010/user failed and that not what I want.
P.S. fetch is npm node-fetch module
No, throw does not jump to catch. It does reject the promise, and all error handlers installed on it will be invoked. In your case, that's the error handler installed by the then call. Notice that .catch(handler) is just sugar for .then(null, handler).
Your current code works like
try {
try {
try {
var data = fetch(`http://${api.host}:${api.port}/user`)
} catch(err) {
throw new Error(`Couldn\'t fetch user data from server: ${err.message}`);
}
var eparkUser = data.json();
} catch(err) {
throw new Error(`Couldn't parse returned json: ${err.message}`);
}
for (var key in eparkUser) {
if (eparkUser.hasOwnProperty(key) && !user.hasOwnProperty(key)) {
user[key] = eparkUser[key];
}
}
done(null, user);
} catch(e) {
done(e);
}
To solve your problem, you'll need to nest your handlers, and install the JSON-parse-handler only on that particular promise:
fetch(`http://${api.host}:${api.port}/user`)
.then(function (data) {
return data.json()
.then(function (eparkUser) {
for (var key in eparkUser) {
if (eparkUser.hasOwnProperty(key) && !user.hasOwnProperty(key)) {
user[key] = eparkUser[key];
}
}
return user;
}, function(err) {
throw new Error(`Couldn't parse returned json: ${err.message}`);
});
}, function(err) {
throw new Error(`Couldn\'t fetch user data from server: ${err.message}`);
})
.then(done.bind(null, null), done);
I'm using the following two pieces of code :
Store.addUser(newUserInfo).then(function(firstResult) {
Store.getUserList().then(function(result){
console.log('this side');
console.log(result);
io.sockets.emit('userAdded', {
userMapByUserId: result
});
}, function(error) {
console.log('List of users could not be retrieved');
console.log(error);
io.sockets.emit('userAdded', {
userMapByUserId: []
});
}
);
}, function(rejection) {
socket.emit('userNotAdded', {
userId: -1,
message: rejection.reason,
infoWithBadInput: rejection.infoWithBadInput
});
});
and in Store :
var addUser = function(newUserInfo) {
var validationResult = Common._validateUserInfo(newUserInfo);
if (validationResult.isOK) {
return keyValueExists('userName', newUserInfo.userName).then(function(userNameAlreadyExists) {
if (userNameAlreadyExists) {
validationResult = {
isOK: false,
reason: 'Username already exists',
infoWithBadInput: 'userName'
};
return Promise.reject(validationResult);
} else {
var newUserId = generateUserId();
//TODO: change it somehting more flexible. e.g. a predefined list of attributes to iterate over
var newUser = {
'userName': newUserInfo.userName,
'password': newUserInfo.password,
'userId': newUserId,
'lastModificationTime': Common.getCurrentFormanttedTime(),
'createdTime': Common.getCurrentFormanttedTime()
};
var user = new User(newUser);
user.save(function(err) {
if (err) {
console.log(err);
console.log('There is a problem saving the user info');
return Promise.reject('There is a problem saving the user info');
} else {
console.log('A new user added: ');
console.log(newUser);
//return getUserList();
return Promise.accept(newUser);
}
});
}
});
} else {
return Promise.reject(validationResult);
}
};
But in the first code , when I do Store.addUser(newUserInfo) it always runs the first function (resolve function) which shouldn't be the case if we do return Promise.reject() in addUser. Any idea on why this happens ?
You've got two return statements too few, two too much, and are overlooking a non-promisified function call.
Store.addUser(newUserInfo).then(function(firstResult) {
return Store.getUserList().then(function(result){
// ^^^^^^
…
This one is not really problematic, as you don't chain anything after the resulting promise, but it shouldn't be missed anyway.
…
return keyValueExists('userName', newUserInfo.userName).then(function(userNameAlreadyExists) {
if (userNameAlreadyExists) {
…
} else {
…
var user = new User(newUser);
user.save(function(err) { … });
// ^^^^
}
});
In this then-callback, you are not returning anything from your else branch. The promise is immediately fulfilled with undefined, and the ongoing save call is ignored - your promises don't know about it, so they can't await it. That's why Store.getUserList() that follows next in the chain doesn't see the changes; they're not yet stored.
It's also the reason why your Promise.reject inside that callback is ignored, and why Promise.accept never caused any problems.
You will need to create a new promise for the result of the save invocation here (so that you actually can return it):
…
var user = new User(newUser);
return new Promise(function(resolve, reject) {
user.save(function(err) {
if (err) {
console.log(err);
console.log('There is a problem saving the user info');
reject('There is a problem saving the user info');
} else {
console.log('A new user added: ');
console.log(newUser);
resolve(newUser);
}
});
}); // .then(getUserList);