DEV Community

loading...

Avoiding exceptions in ReScript

John Jackson
I’m probably riding my bicycle or coding.
Updated on ・5 min read

This article was originally written with ReasonML. I updated it in May 2021 to use ReScript.

Never raising exceptions is commonly considered a best-practice in ReScript code. To a newcomer, that may seem like a lofty goal, but it’s actually quite ordinary in ReScript. It doesn’t mean that we never have bugs, but ReScript’s powerful type system is able to represent every possible state in your application, even erroneous ones. This is so common, ReScript has built-in types for this. Instead of raising an exception, leading to a potential crash, wrap your values in variants and handle them like any other state.

Learning to love option

ReScript provide the option type out of the box. It’s the closest we have to a “nullable” type, and it’s composed of two variants: None and Some('value). It’s likely the most common variant you’ll encounter.

Suppose you have an a Js.Dict and try to get an item that doesn't exist on it. In JavaScript, you would get undefined. In some languages, you’ll raise an exception. In ReScript, you will get None. Combined with pattern matching, it means you always have every possible situation covered.

Problems with the standard library

Even though option is seemingly the solution to our problems, the built-in standard library (inherited from OCaml) is happy to raise exceptions anyway. To make it worse, it usually isn’t obvious which functions can raise exceptions or why.

let planets = ["Mecrury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let earth = planets[2] /* Returns "Earth". So far, so good... */
let pluto = planets[8] /* Bad! This raises an exception. */
Enter fullscreen mode Exit fullscreen mode

Solution: Belt

Belt is the standard library replacement included with ReScript. It covers a lot of what the regular standard library does, but is especially optimized for JavaScript environments.

One of Belt’s features is that it only raises exceptions in its functions explicitly named with Exn. Otherwise, it will return an option type.

let earth = Belt.Array.get(planets, 2) /* This returns Some("Earth") */
let pluto = Belt.Array.get(planets, 8) /* Good! This safely returns None. (Well, maybe not so good for poor Pluto.) */
Enter fullscreen mode Exit fullscreen mode

To make life easier for yourself, put this at the top of your .re files:

open Belt
Enter fullscreen mode Exit fullscreen mode

Or put this in your bsconfig.json:

{
  "bsc-flags": ["-open Belt"]
}
Enter fullscreen mode Exit fullscreen mode

Now calling Array.get will actually call Belt.Array.get. This applies to every other Belt module as well: List, Map, and so on.

Complications with special array access

If you always open Belt, then you won’t have to worry about this one. Otherwise, this error can creep up on you unexpectedly.

You probably don’t want to always write Array.get, especially if you have a lot of arrays. Fortunately, ReScript lets us use the familiar JavaScript syntax: myArray[index]. But if you look at the compiled JavaScript, it actually compiles to an Array.get function. (The regular JavaScript method of accessing an array value is unsafe, since JavaScript does no checks to see if the value exists.)

If you don’t have Belt opened, then the compiled output will use the OCaml Array module to get and set values. If you open Belt, then the output will use the Belt functions instead.

let earth = planets[2] /* Sugar for Array.get(planets, 2) Can raise an exception. */
open Belt
let earth = planets[2] /* This is now sugar for Belt.Array.get(planets, 2) Returns an option. */
Enter fullscreen mode Exit fullscreen mode

If you’ve ever added open Belt to a file and suddenly got type errors on all of your array values, then this is probably why. They were all turned into option types.

Don’t use polymorphic compare

ReScript has a powerful and dangerous feature called “polymorphic compare.” It’s most often seen in one of these forms: the == operator or the compare function. If you’re using these with basic types, floats, integers, etc., then you probably won’t have a problem. But once you start dealing with complex data types, then things can get hairy.

Long story short, == and compare are coded to try to compare any random data type you can throw at it, even if they don’t know what it is or how to compare it. This can lead to unexpected results or, worse, raised exceptions. Good coding hygiene can prevent it from being a problem.

  • Only use compare when you have basic types. If the compiler knows that it’s comparing integers, for example, it will compile to a safe (and optimized) integer comparison function instead of the polymorphic function.
  • If you need to compare complex types, write your own function for it. The convention is to call this function cmp. (Tip: when comparing complex types, convert them to a basic type like integer and compare those instead.)
  • Don't use ==.
  • If you need a more robust function than ===, write your own == function, like so: let (==) = (a, b) => {...code goes here...}.

The compiler can find cases of polymorphic compare for you with warning 102. You can read more about configuring warnings and errors here.

String-to-number conversions

ReScript includes a couple of built-in functions for converting strings to integers or floats. If the conversion isn’t successful, then they raise exceptions. Fortunately, Belt has exception-free alternatives.

  • Instead of int_of_string, use Belt.Int.fromString.
  • Instead of float_of_string, use Belt.Float.fromString.

Supercharge your variant skills

Our array example was simple; the value either exists or it doesn’t. But sometimes a state isn’t binary. Imagine that you’re loading the value from a remote database. You could successfully get the value if it exists, but it also might not exist, or you might have a connection error, or you may just be waiting to load.

In this case, you can roll your own variant: type item('value, 'error) = | Loading | Loaded('value) | Error('error);. When you write your switch block to handle it, you must now account for every possible state, including the error. Some libraries like Relude include modules for this exact purpose.

In conclusion

Exceptions have their place, but they should be reserved for exceptional situations. One downside to exceptions is that the compiler can’t warn you that a function may throw one. Because of this, it’s easy to mistakenly believe your code is safe until it crashes unexpectedly. By following the guidelines above, you can avoid that nasty surprise.

Discussion (0)