Express - Error Handling Doesn't Work with Async Function - javascript

So this is the POST request for /products, when a form is submitted, this function will be invoked. I use a try-catch to catch the error if the form is submitted wrongly.
This is my Schema.
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
price: {
type: Number,
required: true,
min: 0
},
category: {
type: String,
lowercase: true,
enum: ['fruit', 'vegetable', 'dairy']
}
});
The error is with the newProduct.save() line, so if I submit a form that goes against the Schema, like not having a name, I will get an error instead of getting redirected to the page.
app.post('/products', (req, res, next) => {
try {
const newProduct = new Product(req.body);
newProduct.save();
res.redirect(`/products/${newProduct._id}`);
}
catch (e) {
next(e);
}
});
This is my error handler.
app.use((err, req, res, next) => {
const { status = 500, message = 'Something went wrong!' } = err;
res.status(status).send(message);
});

The save method is asynchronous and returns a promise. In your case, newProduct.save() returns a promise which is not being fulfilled and no error is actually thrown:
app.post('/products', async (req, res, next) => {
try {
const newProduct = new Product(req.body);
await newProduct.save();
res.redirect(`/products/${newProduct._id}`);
}
catch (e) {
next(e);
}
});

The best solution would be validate req.body with a validator and save newProduct after it is validated. It'd be better to return the newProduct after it is saved rather than redirecting to certain endpoint.
If it is not validated you can throw your custom error.
I recommend using JOI which is very easy to use.

Related

Struggling to patch a record in mongodb, unique not working and req.body undefined

I'm learning node.js and it's amazing, especially with mongo, but sometimes I struggle to solve a simple problem, like patching only 1 attribute in my user database.
It's easier to patch something that cannot be unique, but I want to patch an username attribute and I defined it as "unique" in my schema. I don't know why, but MongoDB doesn't care other db entry has the same user, it let me save.
My schema:
/** #format */
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema(
{
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
email: { type: String, required: true, unique: true },
userNumber: { type: Number, required: true },
description: { type: String },
verified: { type: Boolean, default: false },
isAdmin: { type: Boolean, default: false },
isSubscriber: { type: Boolean, default: false },
isDisabled: { type: Boolean, default: false },
acceptedTerms: { type: Number, required: true },
},
{ timestamps: true }
);
module.exports = mongoose.model('User', userSchema);
On my user controllers in node, I want to updateOne({ _id: userId}, { username: myNewUsername} but it always happens, it doesn't take into consideration another db entry can have the username, so I tried a different strategy but it doesn't work:
exports.changeUsername = (req, res, next) => {
// Requirements
const userId = req.params.userId;
const newUsername = req.body.username;
console.log('userId: ' + userId);
console.log('newUsername: ' + req.body.username);
User.findOne({ username: req.body.username })
.then(result => {
console.log(result);
if (result.username) {
const error = new Error('Could not find this sport');
error.code = 'DUPLICATED';
throw error;
}
return;
})
.catch(err => next(err));
// if no username was in use then updateOne
User.updateOne({ _id: userId }, { username: newUsername })
.then(result => {
res.status(200).json({
message: 'username has been updated',
username: result.username,
});
})
.catch(err => next(err));
};
I don't know if I can updateOne at the same time add some find validation. What I am doing wrong? Users cannot have the same username.
On the console, it seems it works, but it throws an extra error I don't understand:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at new NodeError (node:internal/errors:371:5)
at ServerResponse.setHeader (node:_http_outgoing:576:11)
at ServerResponse.header (/Users/username/Sites/pipi-api/node_modules/express/lib/response.js:776:10)
I tried this other approach and it works, but doesn't trigger an error if the record is not unique as I stated in the schema.
// GET ONLY ONE SPORT BY ID
exports.changeUsername = async (req, res, next) => {
// Requirements
const userId = req.params.userId;
const newUsername = req.body.username;
console.log('userId: ' + userId);
console.log('newUsername: ' + req.body.username);
try {
const oldUsername = await User.findOne({ username: newUsername });
if (oldUsername.username) {
throw new Error('Error: its duplicated');
}
const user = await User.findOneAndUpdate(
{ _id: userId },
{ username: newUsername },
{ new: true }
);
console.log('User successfully updated.');
return res.status(200).json({ success: true, user });
} catch (err) {
console.log('ERROR: ', err);
return res.status(400).json({ success: false });
}
};
If I uncomment the code above, it triggers an error if I find a record on the database that matches but it doesn't allow me to continue to my next line of codes I the username is not found on the db.
I get a new error:
userId: 6231bdef334afbde85ed9f43
newUsername: tetete
ERROR: TypeError: Cannot read properties of null (reading 'username')
at exports.changeUsername (/Users/user/Sites/pipi-api/v1/controllers/users/index.js:43:21)
That error is not related to Mongo. It means that you are trying to send a response and the response is already sent.
The issue is because you called both User.findOne and User.updateOne and both of them has .then handler. So the first one of these that finishes will send the actual response. In the moment the second one finished, the response is already send and the error is thrown because you are trying to send response again.
Mongo will throw the error if you try to change username property that some other user already have. You should check if the req.params.userId and req.body.username sent correctly to the backend. Try to console.log() them and check if they are maybe null.
Consider refactoring your handler to use async/await instead of then/catch. You can do it like this:
exports.changeUsername = async (req, res, next) => {
try {
const userId = req.params.userId;
const newUsername = req.body.username;
const user = await User.findOneAndUpdate({ _id: userId }, { username: newUsername }, { new: true });
console.log('User successfully updated.');
return res.status(200).json({ success: true, user });
} catch (error) {
console.log('ERROR: ', error);
return res.status(400).json({ success: false });
}
}

.save() not correctly saving to mongoDB with async/await

I'm having an issue with refactoring a function used to create a "post", which then saves it on a "user". It works just fine with the .then() syntax, but I can't seem to figure out how to make this work with async/await.
The post is created, and when I look at the User it is supposed to be saved to, the post id shows up on the User. However, the Post never gets a reference to the User id when created. This is what I have currently.
const create = async (req, res) => {
const userId = req.params.id;
try {
const foundUser = await db.User.findById(userId);
const createdPost = await db.Post.create(req.body);
foundUser.posts.push(createdPost._id);
await foundUser.save((err) => {
if (err) return console.log(err);
});
res.json({ post: createdPost });
} catch (error) {
if (error) console.log(error);
res.json({ Error: "No user found."})
}
}
EDIT: As requested, here is a snippet of my schema for posts.
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const postSchema = new Schema(
{
title: {
type: String,
required: true,
maxlength: 100,
},
description: {
type: String,
maxlength: 300,
},
date: {
type: Date,
default: Date.now(),
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment",
},
],
},
{ timestamps: true }
);
const Post = mongoose.model("Post", postSchema);
module.exports = Post;
The issue is probably here, you're saving the document, but the await here does nothing since you're passing a callback function, so your code does not wait for the response.
await foundUser.save((err) => {
if (err) return console.log(err);
});
There's no need to catch any errors here either since you're in a try catch, so the correct line of code here would be
await foundUser.save()
So, I decided to take a look back at my way of doing this function while using .then(), and I noticed there was a line that I at first thought was unnecessary. I added req.body.user = userId after finding the User. This then gave me the reference to the User on my Post. So, I tried this with my async-await version and it worked! I'm not sure if this is the "right" way to go about this though.
Below I've included the working code:
const create = async (req, res) => {
const userId = req.params.id;
try {
const foundUser = await db.User.findById(userId);
req.body.user = userId;
const createdPost = await db.Post.create(req.body);
foundUser.posts.push(createdPost._id);
await foundUser.save();
res.json({ post: createdPost });
} catch (error) {
if (error) console.log(error);
res.json({ Error: "No user found."})
}
}

Mongoose Throw Error When Non Existing Key is Present

I have a code where I am updating my schema object with request body. I have applied validation rules on the schema. The problem is, I want the schema to throw an error when there's a non existing field in the request body. Non existing key doesn't save to the database as I want but I want to throw some error instead of saving the object. Schema:
const peopleSchema = new mongoose.Schema(
{
fullname: {
type: String,
required: [true, "fullname is required"],
validate: [(value) => isAlpha(value, "en-US", {ignore: " "}), "name should be alphabetic only"],
},
phone: {
type: String,
validate: [isPhone, "please enter a valid phone number"],
},
address: String,
},
{ timestamps: true }
);
Code to update person:
router.put("/:id", checkUser, async (req, res, next) => {
try {
const { id } = req.params;
const user = req.currentUser;
const person = user.people.id(id);
person.set(req.body);
const response = await user.save();
res.json({ response });
} catch (err) {
next(new BadRequestError(err));
}
});
for validation there are two way based on callback and async approache ,
because your code is based on async/await you must to use validateSync() like the following code:
let errors = user.validateSync()//check validation
if(errors){
console.log(errors)
throw errors;//handle your error
}
const response = await user.save()
in callback method :
user.save(function(err,response){
if (err){
console.log(err);
//handle error
}
else{
console.log(response)
res.json({ response });
}
})

JavaScript NodeJs - How to refactor this bit

So I am in a middle of creating a simple CRUD application, but I stumbled with getting MongoDB auto-incrementing value of latest created account.
To be more specific, I have written the tidbit below to enable the following:
1) When registering, do some validation checks
2) Check what was the latest account number, increment by 1
3) Create a new user, add to the DB
Now, if you see below, I've marked three EXHIBITS
1 & 2) For some odd reason, if I remove the code from the route itself, then it stops working properly, but I have no idea how to get rid of repeating code since the functions are pretty much identical, but removing either of those just breaks the sequence. How can I fix this and make my code neater?
3) How would I go about extracting this function into a separate one? After fiddling with this, I only get to the point where "accountNumber is not defined".
const getLastAccountNumber = function() {
User.find({}, { accountNumber: 1, _id: 0 }) **// EXHIBIT 1**
.sort({ accountNumber: -1 })
.limit(1)
.then(function(doc) {
if (!doc) throw new Error("Error?");
accountNumber = doc[0].accountNumber;
return doc[0].accountNumber;
});
};
// TODO: Refactor methods
router.post(
"/register",
[check("email").isEmail(), check("password").isLength({ min: 4 })],
function(req, res) {
User.find({}, { accountNumber: 1, _id: 0 }) **// EXHIBIT 2**
.sort({ accountNumber: -1 })
.limit(1)
.then(getLastAccountNumber())
.then(function() { **// EXHIBIT 3**
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
const { email, password } = req.body;
const amount = 0;
accountNumber++;
const user = new User({
email,
password,
accountNumber,
amount
});
user.save(function(err) {
if (err) {
console.log(err);
res.status(500).send("Error registering new user");
} else {
res.status(200).send("User successfully added");
}
});
});
}
);
Really appreciate any feedback!
Relating to your error, I believe defining the variable by adding let accountNumber; to the top of the file might be enough to get your code working, (though I do not believe it to be a great solution...), though as you asked about refactoring:
embracing promises: Imagine a water/data flowing through a series of pipes, and in each step, the data can be transformed. Keeping this linear flow often makes the code clean and easy to follow. If any error happens on the way, all pipes until "catch" are bypassed.
if an error occurs and we get straight to "catch", we might want to handle the situation differently depending on the failure reason. Thus, we can add error wrappers like ValidationError to check for.
furthermore, we can name the pipes properly, like getNewAccountNumber, which will work even when there are no accounts in the db
arrow functions are nice
// error handling
class ValidationError {
constructor (errors) {
this.errors = errors
}
}
const checkValidation = (req, res)=> {
const errors = validationResult(req)
return errors.isEmpty()
? Promise.resolve()
: Promise.reject(ValidationError(errors.array()))
}
const successResponse = (req, res, data)=> ()=> res.status(200).send(data)
const errorResponse = (req, res, message = 'Internal Server Error')=> error=>
error instanceof ValidationError ? res.status(422).json({ errors: error.errors })
: (console.error(error), res.status(500).send(message))
// utils
const initialAccountNumber = 0
const getNewAccountNumber = ()=> User
.find({}, { accountNumber: true, _id: false })
.sort({ accountNumber: -1 })
.limit(1)
.then(xs=> !xs || !xs.length
? initialAccountNumber
: xs[0].accountNumber + 1)
// route
router.post('/register', [
check('email').isEmail(),
check('password').isLength({ min: 4 })
], (req, res)=> checkValidation(req, res)
.then(getNewAccountNumber)
.then(newAccountNumber=> {
const { email, password } = req.body
return new User({
email,
password,
accountNumber: newAccountNumber,
amount: 0,
})
})
.then(user=> user.save())
.then(successResponse(req, res, 'User successfully added'))
.catch(errorResponse(req, res, 'Error registering new user'))
)
Anyhow, I would prefer to do this as one transaction, if possible by using existing db-build-in solutions (eg. the _id is already "guaranteed" to be unique, the accountNumber using this solution not as much).
You can do something like this:
const getLastAccountNumber = function() {
return User.find({}, { accountNumber: 1, _id: 0 })
.sort({ accountNumber: -1 })
.limit(1)
.then(function(doc) {
if (!doc) throw new Error("Error?");
return doc[0].accountNumber;
});
};
router.post(
"/register",
[check("email").isEmail(), check("password").isLength({ min: 4 })],
function(req, res) {
getLastAccountNumber().then((accountNumber) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
const { email, password } = req.body;
const amount = 0;
accountNumber++;
const user = new User({
email,
password,
accountNumber,
amount
});
user.save(function(err) {
if (err) {
console.log(err);
res.status(500).send("Error registering new user");
} else {
res.status(200).send("User successfully added");
}
});
});
}
);

MongoDB/mongoose - Post save hook not running

I have this model/schema:
const InviteSchema = new Schema({
inviter: {type: mongoose.Schema.Types.ObjectId, ref: 'Account', required: true},
organisation: {type: mongoose.Schema.Types.ObjectId, ref: 'Organisation', required: true},
sentTo: {type: mongoose.Schema.Types.ObjectId, ref: 'Account', required: true},
createdAt: {type: Date, default: new Date(), required: true}
});
InviteSchema.post('save', function(err, doc, next) {
// This callback doesn't run
});
const Invite = mongoose.model('Invite', InviteSchema);
module.exports = Invite;
Helper function:
exports.sendInvites = (accountIds, invite, callback) => {
let resolvedRequests = 0;
accountIds.forEach((id, i, arr) => {
invite.sentTo = id;
const newInvite = new Invite(invite);
newInvite.save((err, res) => {
resolvedRequests++;
if (err) {
callback(err);
return;
}
if (resolvedRequests === arr.length) {
callback(err);
}
});
});
};
And the router endpoint which calls the helper function:
router.put('/organisations/:id', auth.verifyToken, (req, res, next) => {
const organisation = Object.assign({}, req.body, {
updatedBy: req.decoded._doc._id,
updatedAt: new Date()
});
Organisation.findOneAndUpdate({_id: req.params.id}, organisation, {new: true}, (err, organisation) => {
if (err) {
return next(err);
}
invites.sendInvites(req.body.invites, {
inviter: req.decoded._doc._id,
organisation: organisation._id
}, (err) => {
if (err) {
return next(err);
}
res.json({
error: null,
data: organisation
});
});
});
});
The problem here is that the .post('save') hook doesn't run, despite following the instructions, i.e. using .save() on the model instead of .findOneAndUpdate for example. I've been digging for a while now but I cannot see what the problem here could be.
The Invite document(s) are saved to the database just fine so the hook should fire, but doesn't. Any ideas what could be wrong?
You can declare the post hook with different number of parameters. With 3 parameters you are treating errors, so your post hook will be called only when an error is raised.
But, if your hook has only 1 or 2 parameters, it is going to be executed on success. First parameter will be the document saved in the collection, and second one, if passed, is the next element.
For more information, check official doc: http://mongoosejs.com/docs/middleware.html
Hope it helps.

Categories

Resources