In my mongoose schema, I have a field type that is required. I am using a custom error handler in express defined as
const notFound = (req, res, next) => {
const error = new Error(`Not found-${req.originalUrl}`);
res.status(404);
next(error);
};
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
});
};
and use the error handlers at the bottom of my server.js file as
app.use(notFound);
app.use(errorHandler);
However, when I try to test the route that posts an entry using Postman, the request will be stuck and no response is sent back, and in the terminal there is an error saying that UnhandledPromiseRejectionWarning: ValidationError: ...
My question is: shouldn't my custom error handler catch the error?
Make sure that you're forwarding the error wherever you're performing your mongoose action. There you should be able to forward that to your middleware.
For example you can try something like this:
example.findById(req.id, async function(err, foundRecord){
if(err) {
next(err);
} else {
....
}
});
Let me know if that works.
You can try with this simple HttpError:
Create HttpError.js:
class HttpError extends Error {
constructor(statusCode, message = 'Internal Server Error') {
super(message);
this.statusCode = statusCode;
this.isSuccess = false;
this.isError = true;
this.errorMessage = message;
this.data = null;
}
}
module.exports.HttpError = HttpError;
You can create for an example: NotFoundError extends HttpError :
const { HttpError } = require('../HttpError');
class NotFoundError extends HttpError {
constructor(message = 'Not Found') {
super(404, message);
}
}
module.exports.NotFoundError = NotFoundError;
Now, in your app.js, you can handle error like this code below:
// handle notFoundError
app.use((req, res, next) => { throw new NotFoundError() });
// handle unexpectedly error or another error
app.use((error, req, res, next) => {
res.status(error.statusCode || 500).send(error);
})
If you want to look at the tutorial, for mys, but use the indonesian language ("But you don't have to listen, just look at the concept"), you can check it out here:
Handle Express Error: https://www.youtube.com/watch?v=GwS6KJmO9w8&list=PLREvIK3N7Ga6F669gbDCDMwrn37Uq32-O&index=13
Handle HttpError: https://www.youtube.com/watch?v=VoorNGvDypE&list=PLREvIK3N7Ga6F669gbDCDMwrn37Uq32-O&index=14
Or for example code, you can check it out on this github repo: https://github.com/12bedeveloper/basic-express
Keep learn.
Related
I a trying to implement a rest API for our project then I go for node js and express. I have built all the models and controllers. I faced an issue while trying to handle an error. Errorhandler function doesn't receive all the properties of error that caught in try/catch block. I can not read its name in a handler but I can use its name in the controller. Could you please help me?
const errorHandler = (err, req, res, next) => {
console.log(`Error in method:${req.method}: ${err.stack}`.bgRed);
let error = { ...err };
console.log(`Error handler: ${err.name}`);
res.status(error.statusCode || 500).json({
success: false,
data: error.message || 'Server Error',
});
};
module.exports = errorHandler;
controller
const mongoose = require('mongoose');
const Product = require('../models/Product');
const ErrorResponse = require('../utils/error');
const routeName = 'PRODUCT';
// #desc getting single product via id
// #route GET api/v1/products
// #acces public
exports.getProdcut = async (req, res, next) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return next(
new ErrorResponse(`Product not found with id:${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: product,
});
} catch (err) {
console.log(err.name);
console.log('ERRO APPEND');
next(new ErrorResponse(`Product not found with id:${req.params.id}`, 404));
}
};
Assuming that errorHandler is part of your middleware that is somewhere after getProdcut, you can try just throwing the error and Express will automatically detect that for you, because error handling middleware such as yours accepts 4 parameters. So the following would work:
const getProdcut = async (req, res, next) => {
try {
// ...
} catch (err) {
throw err;
}
};
const errorHandler = (err, req, res, next) => {
if (err) {
console.log('hello from the error middleware');
console.log(err.name);
}
else {
// next() or some other logic here
}
}
app.use('/yourRoute', getProdcut, errorHandler);
And inside of your errorHandler you should have access to the error object.
Error-handling middleware always takes four arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don’t need to use the next object, you must specify it to maintain the signature. Otherwise, the next object will be interpreted as regular middleware and will fail to handle errors.
https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling
I am trying to implement express error handling middleware. It is working but I don't understand why I have to set error.message equal to the err.message after I copy err to error. Is the spread operator not copying all the attributes from err? When I inspect the err object I don't even see a message or a stack attribute. Also, why is it able to log the error message before the error message is created in the if block.
This is how I am extending the Error
// src/utils/errorResponse.js
class ErrorResponse extends Error {
constructor(message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
module.exports = ErrorResponse
Custom error handler middleware
// src/middleware/error.js
const ErrorResponse = require('../utils/errorResponse')
const errorHandler = (err, req, res, next) => {
let error = { ...err }
console.log({ error.message }) // undefined
error.message = err.message
console.log({ error.message }) // `Resource not found with id of ${err.value}`
// Log to console for dev
// console.log(error.stack)
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = `Resource not found with id of ${err.value}`
error = new ErrorResponse(message, 404)
} else {
console.log(error.name)
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
})
}
module.exports = errorHandler
I trigger the error by making a get request with a bad ObjectID
// src/controllers/bootcamp.js
const Bootcamp = require('../models/Bootcamp')
...
// #desc Get single bootcamp
// #route GET /api/v1/bootcamps/:id
// #access Public
exports.getBootcamp = async (req, res, next) => {
try {
const bootcamp = await Bootcamp.findById(req.params.id)
if (!bootcamp) {
return next(err)
}
res.status(200).json({ success: true, data: bootcamp })
} catch (err) {
next(err)
}
}
This is happening because the err parameter you're getting is of Error type object and message key is present in prototype of Error object and spread operator shallow copies keys of object ( excluding prototype keys )
Statement for spread operator from mdn -
The Rest/Spread Properties for ECMAScript proposal (ES2018) added spread properties to object literals. It copies own enumerable properties from a provided object onto a new object.
Shallow-cloning (excluding prototype) or merging of objects is now possible using a shorter syntax than Object.assign().
For reference -
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Test
it('should fail trying to GET bookmarks with false user id',async () => {
try {
const response = await request(app)
.get(baseApiUrlUnderTest + 'false_user_id/bookmarks')
.set('Authorization', bearerToken);
} catch (e) {
console.log(e); //it doesn't reach this point
expect(e.httpStatus).to.equal(HttpStatus.UNAUTHORIZED);
}
});
The relevant part of the method under test:
/* GET bookmark of user */
personalBookmarksRouter.get('/', keycloak.protect(), wrapAsync(async (request, response) => {
userIdTokenValidator.validateUserIdInToken(request);
...
}));
where wrapAsync makes sure the error is passed to the custom error handler:
let wrapAsync = function (fn) {
return function(req, res, next) {
// Make sure to `.catch()` any errors and pass them along to the `next()`
// middleware in the chain, in this case the error handler.
fn(req, res, next).catch(next);
};
}
The validateUserIdInToken method which causes the method under test to throw an exception:
const AppError = require('../models/error');
const HttpStatus = require('http-status-codes');
let validateUserIdInToken = function (request) {
const userId = request.kauth.grant.access_token.content.sub;
if ( userId !== request.params.userId ) {
throw new AppError(HttpStatus.UNAUTHORIZED, 'Unauthorized', ['the userId does not match the subject in the access token']);
}
}
module.exports.validateUserIdInToken = validateUserIdInToken;
and the custom error handler in the root middleware:
app.use(function(err, req, res, next) {
if (res.headersSent) {
return next(err)
}
if(err instanceof AppError) { //execution lands here as expected and the test stops...
res.status(err.httpStatus);
return res.send(err);
} else {
res.status(err.status || HttpStatus.INTERNAL_SERVER_ERROR);
res.send({
message: err.message,
error: {}
});
}
});
I think you may be approaching this incorrectly. Invalid auth should not raise errors in the app - it's not an error really, is a validation issue.
If the auth fails, simply send the relevant http error code - 401 back to the client.
res.send(HttpStatus.UNAUTHORIZED, 'a message if you want'); // 401
In your route handler:
personalBookmarksRouter.get('/', keycloak.protect(), wrapAsync(async (request, response) => {
const userId = request.kauth.grant.access_token.content.sub;
if ( userId !== request.params.userId ) {
return response.send(HttpStatus.UNAUTHORIZED);
}
...
}));
In your test, check the for status 401:
chai.request(server)
.get('/false_user_id/bookmarks')
.end((err, result) => {
if (err) {
return callback(err);
}
result.should.have.status(401);
});
Thanks to #laggingreflex's comment I missed debugging that the response actually returned with the expected status and error message
The adjusted test case now looks like this:
it('should fail trying to GET bookmarks with false user id',async () => {
const response = await request(app)
.get(baseApiUrlUnderTest + 'false_user_id/bookmarks')
.set('Authorization', bearerToken);
expect(response.status).to.equal(HttpStatus.UNAUTHORIZED);
});
I am following a course from Wesbos on Node.js, and I created a /add GET route that uses the addStore controller:
index.js:
const express = require('express')
const router = express.Router()
const storeController = require('../controllers/storeController')
const { catchErrors } = require('../handlers/errorHandlers')
router.get('/', catchErrors(storeController.getStores))
router.get('/stores', catchErrors(storeController.getStores))
router.get('/add', catchErrors(storeController.addStore))
router.post('/add', catchErrors(storeController.createStore))
router.post('/add/:id', catchErrors(storeController.updateStore))
router.get('/stores/:id/edit', catchErrors(storeController.editStore))
module.exports = router
storeController.js:
exports.addStore = (req, res) => {
res.render('editStore', { title: 'add store'})
}
But when i go to the add route i get the error:
Error: Can't set headers after they are sent.
Any ideas what is wrong?
Here is the editStore and storeform view:
editStore:
extends layout
include mixins/_storeForm
block content
.inner
h2= title
+storeForm(store)
storeForm:
mixin storeForm(store = {})
form(action=`/add/${store._id || ''}` method="POST" class="card")
label(for="name") Name
input(type="text" name="name" value=store.name)
label(for="description") Description
textarea(name="description")= store.description
- const choices = ['wifi','Open-late','Family Friendly', 'vegetarian','licensced']
- const tags = store.tags || []
ul.tags
each choice in choices
.tag.tag__choice
input(type="checkbox" id=choice value=choice name="tags" checked=(tags.includes(choice)))
label(for=choice) #{choice}
input(type="submit" value="Save" class="button")
catchErrors:
/*
Catch Errors Handler
With async/await, you need some way to catch errors
Instead of using try{} catch(e) {} in each controller, we wrap the function in
catchErrors(), catch any errors they throw, and pass it along to our express middleware with next()
*/
exports.catchErrors = (fn) => {
return function(req, res, next) {
return fn(req, res, next).catch(next);
};
};
/*
Not Found Error Handler
If we hit a route that is not found, we mark it as 404 and pass it along to the next error handler to display
*/
exports.notFound = (req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
};
/*
MongoDB Validation Error Handler
Detect if there are mongodb validation errors that we can nicely show via flash messages
*/
exports.flashValidationErrors = (err, req, res, next) => {
if (!err.errors) return next(err);
// validation errors look like
const errorKeys = Object.keys(err.errors);
errorKeys.forEach(key => req.flash('error', err.errors[key].message));
res.redirect('back');
};
/*
Development Error Handler
In development we show good error messages so if we hit a syntax error or any other previously un-handled error, we can show good info on what happened
*/
exports.developmentErrors = (err, req, res, next) => {
err.stack = err.stack || '';
const errorDetails = {
message: err.message,
status: err.status,
stackHighlighted: err.stack.replace(/[a-z_-\d]+.js:\d+:\d+/gi, '<mark>$&</mark>')
};
res.status(err.status || 500);
res.format({
// Based on the `Accept` http header
'text/html': () => {
res.render('error', errorDetails);
}, // Form Submit, Reload the page
'application/json': () => res.json(errorDetails) // Ajax call, send JSON back
});
};
/*
Production Error Handler
No stacktraces are leaked to user
*/
exports.productionErrors = (err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
};
Link to my repo: https://github.com/GiorgioMartini/learnnode/tree/master/maketogether
I have more of a code architecture question about error handling NodeJs Express apps. I am not sure on what is the best pattern for error handling. On that note, what situations should be considered as an error. For instance, is a 401 Unauthorized code considered an error even though this response is expected when sending bad credentials?
When using:
//app.js file
app.use(err, req, res, next){}
I generally tend to only put 5xx errors here which would represent situations in which a database cannot be found or no network connection issue or function failures. As for the rest, I would manually send back a status code, such as a 401, from the controller by explicitly coding res.status(xxx).send(); or something of the sort. But the issue behind what I'm doing is I tend to repeat myself and have to place logging scattered across the app. Is my approach fine? Should I instead be creating multiple error handling middlewares for different ranges of status codes? I need an opnion
I prefer using middleware with your custom error class to deal with this problem.
Let's see a error class, which contains a custom error message, http status code and logLevel if incase you employ logger.
module.exports = class ApiCalError extends Error {
constructor (message, status, logLevel) {
// Calling parent constructor of base Error class.
super(message);
// Capturing stack trace, excluding constructor call from it.
Error.captureStackTrace(this, this.constructor);
// Saving class name in the property of our custom error as a shortcut.
this.name = this.constructor.name;
// You can use any additional properties you want.
// I'm going to use preferred HTTP status for this error types.
// `500` is the default value if not specified.
this.status = status || 400;
this.logLevel = logLevel || 'warn';
}
toResponseJSON () {
return {
success: false,
message: this.message
}
}
};
Now, let's took a look at a controller. We have only sent successful response from this controller, and passed custom errors to middleware.
exports.Login = function(req, res, next) {
const validationResult = validateLoginForm(req.body)
if (!validationResult.success) {
var err = new customError(validationResult.message, 400, 'warn')
return next(err)
} else {
return passport.authenticate('local-login', (err, token, userData) => {
if (err) {
if (err.name == 'IncorrectCredentialsError' || err.name == 'EmailNotVerified') {
var error = new customError(err.message, 400, 'warn')
return next(error)
}
return next(err)
}
return res.json({
success: true,
message: 'You have successfully logged in!',
token,
user: userData
})
})(req, res, next)
}
}
Now, let's take a look at logger and error handlers middlewares. Here, logger will log the errors in api and pass the error to error handlers. These functions would then be used in app.use().
// Import library
var Logger = function(logger) {
return function(err, req, res, next) {
var meta = {
path: req.originalUrl,
method: req.method,
'user-agent': req.headers['user-agent'],
origin: req.headers.origin
}
if (err instanceof customError) {
logger.log(err.logLevel, err.message, meta)
return next(err)
} else {
logger.log('error', err.message, meta)
return next(err)
}
}
}
var ErrorHandler = function() {
return function(err, req, res, next) {
if (err instanceof customError) {
return res.status(err.status).json(err.toResponseJSON())
}else{
return res.status(500).json({
success: false,
message: err.message
})
}
}
}
module.exports = {
Logger,
ErrorHandler
}