Express Error Handling

adidoshi

adidoshi

Posted on February 12, 2022

Express Error Handling

Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Error handling often doesn't get the attention & prioritization it deserves, but it’s important to remember all it takes is one unhandled error leak into your user interface to override all the seconds you helped your users save. There are so many components involved in a successful, functioning web application, it is essential to foolproof your application by preparing for all possible errors and exceptions. Let's begin then..

Overview:

Errors can be divided into two types- operational & programming errors. Programming errors are the bugs that occurs from the developers code, on the other hand operational error, will inevitably happen when users will interact with our web app. It may include invalid paths, server failing to connect & invalid user input. We should be prepared for these errors in advance by creating a global custom error handling middleware.

Error middleware:

Middleware functions in Express come into play after the server receives the request and before the response fires to the client. They have access to the request and the response objects. They can be used for any data processing, database querying, making API calls, sending the response, or calling the next middleware function (using the next() function).

  • Let's take a simple example where the request path doesn't match the defined routes.
    If you try to visit a route other than '/' suppose https://error-handling.adidoshi.repl.co/user, you will see an error -
{"status": 404, "error": "Not found"}
Enter fullscreen mode Exit fullscreen mode

otherwise if error not handled it would be in plain html like -

not found error

Creating an error class -

A common practice is to take the initial Error object and expand on it with our own class.

class ErrorHandler extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith("4") ? "fail" : "error";
    Error.captureStackTrace(this, this.constructor);
  }
}
Enter fullscreen mode Exit fullscreen mode

The super() function only takes message as an argument because that’s what Error takes initially. Then, we add a statusCode property and a status that derives from statusCode. Finally, the captureStackTrace line prevents this class from showing up in the stack trace, which is part of the console log that shows where in code the error occurred.

  • For example, with this error class lets rewrite our above code -
app.use((req, res, next) => {
next(new ErrorHandler(`Can't find ${req.originalUrl} on this server!`, 404));
})
Enter fullscreen mode Exit fullscreen mode

Catching Errors in Async Functions

Mostly while developing an API we come around writing async functions for database query, sending response. Until now, we’ve used try/catch blocks to catch errors in our async/await functions (to give you an example)

const createPost = async (req, res) => {
    const { desc, location, pic } = req.body;
  try {
    if (!desc || !pic || !location) {
        res.status(400).json('Please fill all the details')
      } else {
        const newPost = new Post({
          user: req.user._id,
          desc,
          location,
          img: pic,
        });
        const createdPost = await newPost.save();
        res.status(201).json(createdPost);
      }
  } catch (error) {
      next(error)
    }
  }
Enter fullscreen mode Exit fullscreen mode

but they make our code look messy. The best way to avoid try catch in your node js application is to, wrap your function call into a higher order function.

const catchAsync = fn => {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};
Enter fullscreen mode Exit fullscreen mode

This is a function catchAsync , where I'm passing three parametes req, res, next object which will be passed as standard from our express function, here it means we wrap our func call into Promise & next means that pass it to the next function in the chain.

  • Let's wrap our above createPost function into this -
const createPost = catchAsync(async (req, res, next) => {
  const { desc, location, pic } = req.body;
  if (!desc || !pic || !location) {
    return next(new ErrorHandler("Fill all the details", 400));
  } else {
    const newPost = new Post({
      user: req.user._id,
      desc,
      location,
      img: pic,
    });
    const createdPost = await newPost.save();
    res.status(201).json(createdPost);
  }
});
Enter fullscreen mode Exit fullscreen mode

Wohoo! Finally we get rid of try/ catch, as now any route function you wrap inside this catchasync, that will automatically catch the errors. Note: We also have an NPM package express-async-handler which works in a similar way & inside which we can wrap our route function, but understanding how things works behind the scenes will help us a lot.

Production vs development errors -

We want to send understandable, clean error messages to the user. However, in development we want as much information as possible. We’ll access our environment variable and send back responses accordingly:

  • Stack trace- It is used to trace the active stack frames at a particular instance during the execution of a program. The stack trace is useful while debugging code as it shows the exact point that has caused an error
const sendErrorDev = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
    stack: err.stack,
  });
};
const sendErrorProd = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
  });
};
module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.message = err.message || "Internal Server Error";

  if (process.env.NODE_ENV === "development") {
    sendErrorDev(err, res);
  } else if (process.env.NODE_ENV === "production") {
    sendErrorProd(err, res);
  }
};
Enter fullscreen mode Exit fullscreen mode

To explain the main function code, it says err.statusCode if any or 500(statusCode) which is the error caused by the server.

  • Further, we can also handle mongoose errors which are helpful in case of model properties check if any - Mongoose errors can include 'duplicate key error'
  if (err.code === 11000) {
    const message = `Duplicate ${Object.keys(err.keyValue)} entered`;
    err = new ErrorHandler(message, 400);
  }
Enter fullscreen mode Exit fullscreen mode
  • Generally, when we create express API's we divide our code into a specific structure called Model–view–controller (mvc design pattern), which is good practice to have as a developer. With this we have middleware's which also include error middleware we talked about.

That's it, hope reading this post made you understand proper error handling practices in nodejs & you try it in your upcoming projects. Thankyou for visiting!!

💖 💪 🙅 🚩
adidoshi
adidoshi

Posted on February 12, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related