0

Most of the express error handling documentation and tutorials I've read suggest using Express middleware to throw errors up to, and to handle the errors there.

eg. From the Express documentation:

app.get('/', function (req, res, next) {
  fs.readFile('/file-does-not-exist', function (err, data) {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}

The question I have, is around the use of this when either using external SDKs (like Mongo or GCP) or Axios/Fetch calls to other microservices.

eg.

//GCP example 
app.post('/items/:id', async function (req, res, next) {

    try {
       const id = req.params.id; 
       const data = req.body; 
       const snapshot =  await db.doc(`items/${id}`).create(data); //This will throw an error if that document already exists
       res.send("Success"); 
    } catch(err) {
        next(err); 
    } 
}); 
//Microservice example
app.get('/foo/:id', async function (req, res, next) {

    try {
       const id = req.params.id; 
       const response =  fetch(`/microservice/${id}`); //Say errors with a 404 status or a 500 status
       res.send(response.body); 
    } catch(err) {
        next(err); 
    } 
});


The question is - if we want to now return useful error responses, what's the right way to do this?

For example - the errors throw from the fetch function are going to be a fetch Response object, while the GCP function is going to throw an error that looks like this (I couldn't find documentation for the error object):

{ code: 6,
  metadata: Metadata { _internal_repr: {} },
  details: 'Document already exists: projects/project-id/...' 
}

So I could implement my error handling function to cater towards different types of error objects:

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }

  //Handle Response Objects
  if (err.status) {
     switch(err.status) {
          case 404: return res.status(404).send("Item not found"); 
          // etc
     }
  }

  //Handle GCP errors
  if (err.code && err.details) {
     switch(err.code) {
        return res.status(409).send("Item already exists"); 
     }
  }
}

But this seems like a brittle solution - for example what if another library I was using included 'status' on their error object.

So then, the alternative approach would be to standardise the errors myself at the of the library.

I can throw objects as errors like

   throw {
         status: 404, 
         message: "Item not found'
   }; 

And implement my code like:

formatGcpError(err) {
   switch(err.code) {
       case 6: return {
           status: 409, 
           message: "Item already exists", 
       }
   }
}


app.post('/items/:id', async function (req, res, next) {

    try {
       const id = req.params.id; 
       const data = req.body; 
       const snapshot =  await db.doc(`items/${id}`).create(data); //This will throw an error if that document already exists
       res.send("Success"); 
    } catch(err) {
        next(formatGcpError(err)); 
    } 
}); 

Am wondering - what's the standard approach here?

2
  • Same for any language... Catch the exceptions at the boundary. Interacting with a third-party api? Catch their exceptions and handle them. If that means wrapping them and rethrowing because you do not have enough information here, then do so. Have the root handler take your exceptions and log them, give the user a opsie or some other userland this didn't work response.
    – Kain0_0
    Commented Jun 18, 2019 at 7:07
  • @Kain0_0 - I think your comment suffices as an answer if you want to flesh it out, give some of the key terms/concepts so I can google further.
    – dwjohnston
    Commented Jun 19, 2019 at 4:12

1 Answer 1

1

Same for any language... Catch the exceptions at the boundary. Handle those errors immediately, or translate them to something your application understands and rethrow them.

Third Party Software

To get anything useful done you need to interact with other software. Unfortunately this means interacting with a foreign understanding of the world.

As you've pointed out those three separate external systems report errors in three different ways. This is obviously not extensible. Requiring every inch of your own codebase to understand even just snippets of these foreign codebases not only doesn't scale - its a breading ground for bugs in your own code as these other systems change.

Knowledge

That leaves us with a question: Who knows the most about this external system? The code that interacts with it...

When you interact with a third-party, the code interacting with that third-party has to know about it. Which means they are already dealing with that external model of the world. Having such knowledge makes that point of the code the best suited for handling the outputs of that model. Both the good results, and its errors.

In a sense you a writing a plugin/adaptor. It exposes one or more services in terms that your own applications model can understand. When one of those services is requested, its job is to translate that into something the third-party software understands, even if that takes several steps and has some automatic-recovery behaviour. This also means that its responsible for describing the outcome (good or bad) back to your application.

A good example of this would be File (not in any specific language, take your pick). A File has some method for accessing/altering the bits it holds, along with its size, and maybe some location information (a.k.a. Path).

  • To your application it looks something like an array, a string, an iterator, and enumerable, a range, or ... something that is a concept easily relatable to your application.
  • On the inside it has to handle the nuances of the OS file system, a ZIP file, a Pipe, or something else. The data may need to be paged into memory, and paged out. The data may not be stored linearly but in some custom format. Perhaps it is decompressed or decrypted on the fly as your application requests the next few bytes.

Eitherway, when something happens it still has to communicate that back to your application in a meaningful way. The filesystem might page into memory the data but it is file that translates that into a string when returning from readAllText(). The filesystem might return a number indicating status, yet its File's responsibility to convert that into a BadPathException.

Meaning

While a File abstraction does a stellar job of hiding the file-system, your application is probably not about files. Whatever it is about, files are an implementation detail, it could have been magic storage system number 2 for all your application cares.

What this means is that when something bad happens out in file-system land, the rest of your application will not understand what that means. Does FileNotFound mean that the administrator needs to provide a configuration file? Does it mean that the user typed the path in wrong? Should we immediately re-initialise the file-system?

So the code that interacts with File has to interpret the results. If this were a configuration routine then it might return a Config object when File loads that files data. But if File reports an error it might throw ConfigurationNotFound or ConfigurationAccessFailed.

In the bad case such as ConfigurationAccessFailed a piece of code higher up the call hierarchy might decide to display a question to the user. It could only do this because it could understand what the error was, a FileNotFound error could have been anything.

Pragmatics

Often external systems change, so do the kinds of errors they report. So at the very least you should capture all errors and report them as an error from that external system in the current context. Say UnknownFileConfigurationError. At the very least you are honest with yourself.

But better than that the rest of your application understands how to handle the situation. In this case it should let the stack unwind to a recoverable point. There it will log the error, and if the user needs to be informed it will give a general: oopsie not sure what just happened. We made a note of it/We are falling back to safe behaviour response. Then the code either exits or does the next task.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.