Implementation: Errors

Sh*t happens, but how should you handle it?

Jérôme Beau
5 min readJul 5, 2024

There are many reasons why things might not work as expected, as each of these should be handled differently.

The raise of exceptions

At the beginning were error codes. Programs, typically written in C, were returning -1 or some other code that ought to be interpreted, then actioned upon. However this had several limitations:

  • information was scarce: carrying more info about what did fail, why, etc. required more code to add, once in a while;
  • not handling was possible: if you didn’t think about looking at the returned error code, you might miss the error notification, or miss to notify callers about it.

To fix this, exceptions were invented as objects which:

  • could hold any amount of data about the error and its context, recommendations for fixing it, etc;
  • could be part of a hierarchy of errors, thus allowing to handle a whole family of errors consistently;
  • were “thrown” (or “raised”) to be automatically propagated up to the hierarchy of callers, who might choose to “catch” them or not.
  • could be declared as potentially thrown by some function, so that the callers would be aware as to what to handle, or even be required to handle them (a.k.a. “checked exceptions”).

This occurred as early in the 1960s and was implemented in LisP then the robust Ada, C++, Borland’s Object Pascal, Java in the 1990s and many others, including todays’ JavaScript Errors.

Taxonomy

Speaking of hierarchy, the first split between the population of errors is between:

  • the ones you expect, which you can handle early;
  • the ones you didn’t expect (a stack overflow, a memory shortage, an illegal argument, etc.) which, because you didn’t anticipated how to handle them, may end up interrupting the whole process or have other nefarious implications.

And you must handle both.

Java errors hierarchy, where “checked” errors require to be handled. The “Error” family is not expected (and so not checked) but fatal

Another dimension of errors is their criticality. Some errors:

  • are recoverable, which means that re-trying the operation (automatically or by user request) might eventually succeed;
  • cannot be recovered, i.e. they are fatal.

You’ll probably want to notify recoverable errors as “warnings” rather than errors, then.

Handling

We saw previously the benefits of exceptions and you should of course go for them instead of error codes. Now, what should you do and avoid about them?

App-level handler

Whether you’re on the front or back end, exceptions you didn’t expect will end up reaching the root call of your app, and some last-resort handler should be there to catch them.

This could be some hardcoded code in your main file, but you’d probably benefit from encapsulating its handling into dedicated software item, so that you can easily test it.

Displaying

Now that the exception has been thrown, it’s time to report them. To do so:

  • Don’t display messages directly from them. Most exceptions are design to hold one, but this can only be some “default”, internal message for logs. The actual messages displayed to the user should be inferred from the exception itself (typically an error code in it), if only because those messages should be internationalized.
  • include a display target in them: not all errors are meant to be reported in the same place (the console, a web page, a popup, a monitoring system like Sentry, a combination of all this, etc.). This could be inferred from the exception type, but one size doesn’t fit all and he most revelant place to decide where an error should display is probably the throwing place.
  • use criticality channels such as the usual info/warn/error/debug levels, which will allow to tune information details when debugging. Notably, you will not enable the same channels in development or production environments.
  • always display something, at worse as a debug log; otherwise you might never realize that something went wrong.

Patterns & anti-patterns

  • never absorb errors as stated above. Doing something like catch (e) { // Do nothing } is called “absorbing” errors on the premise that “you know this is ok” because you think you know what e will always be. Instead, at worse log what e is, at the debug level if you will.
  • it’s perfectly fine to catch an error in order to re-throw another, more high-level error instead. However, re-throwing the same error is a smell, since it means the error management is split in several places.
  • Using a context object to customize error handling can be powerful. For instance, delegating error handling to the context of a form might display form errors instead of bubbling up to the app root call.
  • Don’t try to acquire a resource and use it in the same block. This leads to overly complex and faulty code.
  • don’t use exceptions for normal flow. Aside the fact that, depending on the language, they be may be less performant than conditional checks, this would trigger exception breakpoints or system/libraries handlers (sending them to Sentry for instance) and biasing your error metrics, whereas normal handling would have not.

Debugging

Hopefully logging is not the only way to track errors. Debugging can help a lot more than that. I’m always amazed on many developers still rely on logs exclusively, without beneficing of breakpoints (including conditional or exception-triggerred), variables evaluations/edition or call stack navigation.

There is also some coding habits that help or not the debugging activity per se:

  • avoid multiple returns in a function. This can be tempting, but leaving a function early (aside exceptions) might keep you wondering why your late breakpoint is never reached.
  • use different messages for different problems: this might sound obvious but, as stated above, “Could not fetch data” might have different causes. The root cause (now available in browsers and since Node 16.9) or error code, if specified, may help to discriminate, but basically not having two exceptions with the same message in your codebase will help you a lot to quickly track down the origin of a problem.
  • display the calling context of an error, since the same error might occur at different places of a software. Just seeing a message might not be enough to track down where the error is occurring. This will typically be done by displaying the stack trace, but sometimes adding business context in logs will help. This would also help filtering to discard significant amounts of useless logs when hunting down a problem.

--

--

Jérôme Beau
Jérôme Beau

Written by Jérôme Beau

Sharing learnings from three decades of software development. https://javarome.com

No responses yet