Thursday, May 25, 2017

How to Handle Exceptions in Elixir

Exception handling is a great practice for any software development methodology. Whether it's for test-based development, agile sprints, or a hacking session with just a good old todo list, we all can benefit from ensuring our bases are covered with a robust approach to defect handling. 

It's paramount to ensure errors are taken care of, whilst being aesthetically pleasing and of course not becoming a big problem logically with cryptic messages for the end user to try and glean meaning from. If you do that, you certainly are on a great route to make a solid, stable and sticky app that users enjoy working with and will recommend highly to others.

Ideally for us, Elixir provides extensive exception handling via several mechanisms such as try/catch, throws, and the {:error, reason} tuple.

To display an error, use raise in your interactive shell to get a first taste:

We can also add a type to this like so:

How Error Handling Works in Elixir

Some of the ways errors are dealt with in Elixir may not be obvious at first glance.

  • Firstly about processes—in using spawn, we can create independent processes. That means a failure on one thread should not affect any other process, unless there was a linkage in some manner. But by default, everything will stay stable.
  • To notify the system of a failure in one of these processes, we can use the spawn_link macro. This is a bidirectional link, which means that if a linked process terminates, an exit signal will be triggered.
  • If the exit signal is anything other than :normal, we know we have a problem. And if we trap the exit signal with Process.flag(:trap_exit, true), the exit signal will be sent to the process's mailbox, where the logic can be placed on how to handle the message, thus avoiding a hard crash.
  • Finally we have Monitors, which are similar to spawn_links, but these are unidirectional links, and we can create them with Process.monitor
  • The process which invokes the Process.monitor will receive the error messages on failure.

For a sample error, try adding a number to an atom and you will get the following:

To ensure the end user does not get errored out, we can use the try, catch and rescue methods provided by Elixir.

Try/Rescue

First in our toolbox for exception handling is try/rescue, which catches errors produced by using raise so is really best suited for developer errors, or exceptional circumstances such as input error.

try/rescue is similar in usage to a try/catch block you may have seen in other programming languages. Let's look at an example in action:

Here we utilize the try/rescue block and the aforementioned raise to catch the RuntimeError

This means the ** (RuntimeError) default output of raise is not displayed, and is replaced with a nicer formatted output from the IO.puts call. 

As a best practice, you must use the error message to give the user useful output in plain English, which helps them with the issue. We'll look at that more in the next example.

Multiple Errors in a Try/Rescue

A major benefit of Elixir is that you can catch multiple outcomes in one of these try/rescue blocks. Look at this example:

Here we have caught two errors in the rescue

  1. If the file is unable to read.
  2. If the :source_file symbol is missing. 

As mentioned before, we can use this for making easy-to-understand error messages for our end user. 

This powerful and minimal syntax approach of Elixir makes writing multiple checks very accessible for us to check many possible points of failure, in a neat and concise way. This helps us to ensure we don't need to write elaborate conditionals making long-winded scripts that may be hard to visualize fully and debug correctly during later development or for a new developer to join.

As always when working in Elixir, KISS is the best approach to take. 

After

There are situations when you will require a specific action performed after the try/rescue block, regardless of if there was any error. For Java or PHP developers, you may be thinking of the try/catch/finally or Ruby's begin/rescue/ensure.

Let's take a look at a simple example of using after.

Here you see the after being used to constantly make a message display (or this could be any function you wished to throw in there).

A more common practice you will find this used on is where a file is being accessed, for example here:

Throws

As well as the raise and try/catch methods we have outlined earlier, we also have the throw and catch macros.

Using the throw method exits execution with a specific value we can look for in our catch block and use further like so:

So here we have the ability to catch anything we throw inside the try block. In this case, the conditional if x == 3 is the trigger for our do: throw(x).

The output from the iteration produced from the for loop gives us a clear understanding of what has occurred programmatically. Incrementally we have stepped forward, and execution has been halted on the catch.  

Because of this functionality, sometimes it can be hard to picture where the throw catch would be implemented in your app. One prime place would be in usage of a library where the API does not have adequate functionality for all outcomes presented to the user, and a catch would suffice to rapidly navigate around the issue, rather than having to develop much more within the library to handle the issue and return appropriately for it.

Exits

Finally in our Elixir error handling arsenal we have the exit. Exiting is done not through the gift shop, but explicitly whenever a process dies. 

Exits are signaled like so:

Exit signals are triggered by processes for one of the following three reasons:

  1. A normal exit: This happens when a process has completed its job and ends execution. Since these exits are totally normal, usually nothing needs to be done when they happen, much like a exit(0) in C. The exit reason for this kind of exit is the atom :normal.
  2. Because of unhandled errors: This happens when an uncaught exception is raised inside the process, with no try/catch/rescue block or throw/catch to deal with it.
  3. Forcefully killed: This happens when another process sends an exit signal with the reason :kill, which forces the receiving process to terminate.

Stack Traces

At any given update juncture on throw, exit or errors, calling the System.stacktrace will return the last occurrence in the current process. 

The stack trace can be formatted quite a bit, but this is subject to change in newer versions of Elixir. For more information on this, please refer to the manual page.

To return the stack trace for the current process, you can use the following:

Making Your Own Errors

Yep, Elixir can do that also. Of course, you always have the built-in types such as RuntimeError at your disposal. But wouldn't it be nice if you could go a step further?

Creating your own custom error type is easy by using the defexception macro, which will conveniently accept the :message option, to set a default error message like so:

Here's how to use it in your code:

Conclusion

Error handling in a meta-programming language like Elixir has a whole heap of potential implications for how we design our applications and make them robust enough for the rigorous bashing of the production environment. 

We can ensure the end user is always left with a clue—a simple and easy-to-understand guiding message, which won't make their task difficult but rather the inverse. Error messages must always be written in plain English and give plenty of information. Cryptic error codes and variable names are no good to the average users, and can even confuse developers!

Going forward, you can monitor the exceptions raised in your Elixir application and set up specific logging for certain trouble spots, so you can analyze and plan your fix, or you can look at using an off-the-shelf solution.

Third-Party Services

Improve the accuracy of our debugging work and enable monitoring for your apps' stability with these third-party services available for Elixir:

  • AppSignal can be very beneficial for the quality assurance phase of the development cycle. 
  • GitHub repo bugsnex is a great project for using the API interface with Bugsnag to further detect defects in your Elixir app.
  • Monitor uptime, system RAM and errors with Honeybadger, which provides production error monitoring so you don't need to babysit your app.

Extending Error Handling Further

Going forward, you may wish to further extend the error handling capabilities of your app and make your code easier to read. For this, I recommend you check out this project for elegant error handling on GitHub

I hope you have gained a further insight from this guide and will be able to practically handle any exception case you need in your Elixir app now! 


by Tom Whitbread via Envato Tuts+ Code

No comments:

Post a Comment