1

I am building an API in expressjs and, currently, mongo/mongoose. I currently have some model methods that return true or false, some that return a value or false, and some that return a value or throw an error.

This is making my handlers a bit cumbersome / complex so before the project gets any larger I'd like to standardize on one way of handling errors at the model level. I can think of two ways to accomplish this...

Strategy 1. Make all model methods either return a value or throw an error. The handler would wrap the call in a try/catch. Any custom errors I generated would have unique codes to differentiate them to the code in the handler's catch block.

Strategy 2: Make all model methods return an Object and do all the trying and catching within the model methods. The returned Object would always have either a result property or an error and errCode property if there was an error so my handler could check that and respond appropriately.

I'm slightly leaning towards #1 right now but I thought I'd ask here to see if anyone can suggest some good reasons to avoid or embrace either approach - or indeed suggest a third way. Thanks for reading!

1
  • I like strategy 1, but the only thing that matters is that you establish a consistent pattern across the app. – Dan Wilson Oct 30 '20 at 18:17
2

From my standpoint, the biggest weakness of C-style error handling is that programmers can and sometimes (maybe even often depending on your team and coding standards) will ignore errors.

If there's one major advantage to exception-handling as I see it, it's that people can't ignore the errors. If you get your lazy programmer implementing some function wanting to just do the quick and dirty, if he/she calls a function that throws, they can't ignore it. They can't implicitly make it go away like it never existed. It's going to cause a stack to unwind, destroy/collect all objects on the stack, and branch into a catch block.

Your second option defeats that biggest advantage as I see it. Of course, you should distinguish between exceptional and non-exceptional cases. A key not found in a map is probably not reason enough to throw, since many of the times when users create maps of things associated with keys and want to search the map, they're doing it not assuming the key exists but checking if it exists. For a key to not exist is not an exceptional case, but a reasonably common case, so to throw here seems very inefficient both for programmers and CPU. Throwing is relatively expensive (ex: in comparison to searching a key in a hash table) in practically all hardware implementations, since the jumps involved aren't cheap no matter how efficiently they're implemented if the work being done is trivial in cost. But for truly exceptional things, like things that shouldn't happen, runtime inputs that should not be what they are, I'd always favor throwing in a way that even the lazy programmer can't ignore. So I'd vote for #1 in all cases where it makes sense to throw.

2

Your existing approaches of error handling and option 2 both are similar to the Try pattern. You might want to have a look and see if that is something you want to implement more concretely.

As mentioned in the article, this pattern can be cumbersome if overdone, but I found it to be useful specifically for traversing different layers of an application while maintaining full separation of concerns within those layers.

From the above link:

Try, in other words

If you’re familiar with any of these other concepts, they might help draw parallels to Try:

A Try is kind of like an Either where one side is the successful value, and the other is an exception.

A Try is somewhat like an Optional, but instead of present or not, it’s successful or not and, if not, it records the error.

A Try is a bit like a synchronous observable stream, with either a single event or an error.

A Try is basically like a settled Promise, it has either succeeded (fulfilled) or failed (rejected).

The drawback of the first option you suggested is that the caller needs to know how to handle the exceptions from the method being called, which could be specific to that lower layer. Rather than have that error-handling logic live in a higher layer, you can convert the error to a value that the caller could reasonably have knowledge of (as you said, maybe even just a general set of error codes).

1
  • Thanks for the answer. Thankfully my app has very few layers and all I need to do with any given error is give the user minimal feedback and, if it's an unanticipated error, log the stack trace and timestamp so the logic is blessedly simple. I do like that the second option presents a completely consistent API for every method, that would make my handlers very neat. – Roger Heathcote Oct 31 '20 at 8:22
1

The biggest advantage of exceptions is that they can be ignored. If you aren’t taking advantage of that, or don’t expect the users of your API to be taking advantage of that, they are no better than the alternatives, and depending upon your usage pattern much worse (if you are throwing an exception 99% of the time, things are going to be a bit slow).

Note that by ignored I do NOT mean an empty catch block, I mean no catch block. Let the exception bubble up the stack until something knows what to do with it. Not catching exception can be very powerful, but requires planning and thought.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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