Dashiell Rose Bark-Huss
Dashiell Rose Bark-Huss

Reputation: 2975

Node.js When updating a document failed, is best to throw an exception or returning information?

I have an updateDocument method in a class for a service layer in a node.js, express, mongoose application. I'm wondering what is the best practice for handing cases where the update method didn't update a document, for example if the wrong id is passed in.

Version 1: If the update wasn't successful, return an object with success: false:

  async updateDocument(id, updates) {
    const output = await this.DocumentModel.updateOne({ _id: id }, updates);

    let message = 'Something went wrong';
    let success = false;
    let updatedItem = null;
    if (output.nModified) {
      message = 'Successfully updated document.';
      success = true;
      updatedItem = await this.getDocument(id);
    }

    return { message, success, updatedItem};
  }

Version 2: If the update wasn't successful, throw an error:

  async updateDocument(id, updates) {
    const output = await this.DocumentModel.updateOne({ _id: id }, updates);

    let updatedItem;

    if (output.nModified) {
      updatedItem = await this.getDocument(id);
    } else{
      throw new Error("The document wasn't updated")
    }

    return updatedItem;
  }

Do you always throw an exception when the input, such as a bad id, isn't correct? Or could you return information about the update being a success or not? As newbie node.js developer, I'm not sure I am grasping the full picture enough to recognize problems with either method.

Upvotes: 1

Views: 1116

Answers (2)

Dashiell Rose Bark-Huss
Dashiell Rose Bark-Huss

Reputation: 2975

It seemed like there were a lot of different opinions on this and not one go-to method. Here's some information I found and what I ended up doing.

When to throw an exception?

Every function asks a question. If the input it is given makes that question a fallacy, then throw an exception. This line is harder to draw with functions that return void, but the bottom line is: if the function's assumptions about its inputs are violated, it should throw an exception instead of returning normally.

Should a retrieval method return 'null' or throw an exception when it can't produce the return value?

Answer 1:

Whatever you do, make sure you document it. I think this point is more important than exactly which approach is "best".

Answer 2:

If you are always expecting to find a value then throw the exception if it is missing. The exception would mean that there was a problem.

If the value can be missing or present and both are valid for the application logic then return a null.

More important: What do you do other places in the code? Consistency is important.

Where should exceptions be handled?

Answer 1: in the layer of code that can actually do something about the error

Exceptions should be handled in the layer of code that can actually do something about the error.

The "log and rethrow" pattern is often considered an antipattern (for exactly the reason you mentioned, leads to a lot of duplicate code and doesn't really help you do anything practical.)

The point of an exception is that it is "not expected". If the layer of code you are working in can't do something reasonable to continue successful processing when the error happens, just let it bubble up.

If the layer of code you are working in can do something to continue when the error happens, that is the spot to handle the error. (And returning a "failed" http response code counts as a way to "continue processing". You are saving the program from crashing.)

-source: softwareengineering.stackexchange

Answer 2: Handle errors centrally, not within a middleware

Without one dedicated object for error handling, greater are the chances of important errors hiding under the radar due to improper handling. The error handler object is responsible for making the error visible, for example by writing to a well-formatted logger, sending events to some monitoring product like Sentry, Rollbar, or Raygun. Most web frameworks, like Express, provide an error handling middleware mechanism. A typical error handling flow might be: Some module throws an error -> API router catches the error -> it propagates the error to the middleware (e.g. Express, KOA) who is responsible for catching errors -> a centralized error handler is called -> the middleware is being told whether this error is an untrusted error (not operational) so it can restart the app gracefully. Note that it’s a common, yet wrong, practice to handle errors within Express middleware – doing so will not cover errors that are thrown in non-web interfaces.

-source; Handle errors centrally, not within a middleware

So it seems like these two principles disagree. #1 says to handle it right away if you can. So for me it would be in the service layer. But the #2 says handle it centrally, like in the server file. I went with #2.

My decision: throw the error in a custom error class

It combined a few methods people suggested. I am throwing the error, but I'm not "log and rethrow"-ing, as the answer above warned against. Instead, I put the error in a custom error with more information and throw that. It is logged and handled centrally.

So first in my service layer this is how an error is thrown:

async addUser(user) {
   let newUser;
   try {
      newUser = await this.UserModel.create(user);
   } catch (err) {
      throw new ApplicationError( // custom error
         {
            user, // params that are useful
            err, //original error
         },
        `Unable to create user: ${err.name}: ${err.message}` // error message
      );
   }
   return newUser;
}

ApplicationError is a custom error class that takes an info object and a message. I got this idea from here:

In this pattern, we would start our application with an ApplicationError class this way we know all errors in our applications that we explicitly throw are going to inherit from it. So we would start off with the following error classes:

-source: smashingmagazine

You could put other helpful information in your custom error class, even maybe what EJS template to use! So you could really handle the error creatively depending on how you structure your custom error class. I don't know if that's "normal", maybe it's not SOLID to include the EJS template, but I think it's an interesting concept to explore. You could think about other ways that might be more SOLID to dynamically react to errors.

This is the handleError file for now, but I will probably change it up to work with the custom error to create a more informative page. :

const logger = require("./logger");

module.exports = (err, req, res, next) => {
  if (res.headersSent) {
    return next(err);
  }

  logger.log("Error:", err);
  return res.status(500).render("500", {
    title: "500",
  });
};

Then I add that function to my server file as the last middleware:

app.use(handleError);

In conclusion, it seems like there's a bit of disagreement on how to handle errors though it seems more people think you should throw the error and probably handle it centrally. Find a way that works for you, be consistent, and document it.

Upvotes: 1

Helge Drews
Helge Drews

Reputation: 569

There is no golden way, only principles that lead to robust and well-maintainable software.

Generally, you should use a try-catch-statement for all kinds of errors that are not in your control (connections, disk space, credentials, ...) . The errors should then be handled as soon as possible, but not before. The reason for this is that you often don't know, yet, how to handle an error in an appropriate manner at a lower layer.

For logical "errors" that you can expect (wrong input format, missing username, unknown options, ...), you should use an if-statement or a validation function and then throw an error, if anything is not as expected.

In your case, you should check, if the methods updateOne or getDocument can throw errors. If yes, you should wrap these functions with a try-catch-statement.

A few more tips:

  • Both versions of your code are good. But I would prefer version 2 because it is more concise.
  • If you are sure that there is always an output object, you can destruct the nModified property like this:
const { nModified } = await this.DocumentModel.updateOne({ _id: id }, updates);
  • If you use a negative if-statement, you can reduce the depth of indentation and you can use const variables:
if (!nModified) {
    throw new Error("The document wasn't updated")
}

const updatedItem = await this.getDocument(id);
  • Now, you could also directly return this.getDocument(id) and don't need the variable updatedItem anymore.
  • You can finally handle your errors in your controller classes.
  • You can use custom error classes to be consistent in your error handling and error messages all over your app.

I hope this is at least a bit helpful.

References

These are some similar questions with good answers. But you need to take care because many code examples are not in modern JavaScript.

Upvotes: 1

Related Questions