We’ve been hearing a lot about Elm lately, especially how it claims to prevent errors. Our newest contributor, Matt Granmoe, will explore it, how to get started, and some thoughts on error prevention and debugging. Matt will be exploring error handling and debugging in various JavaScript and web technologies with us. Matt is a JavaScript developer, progressive bassist, blogger, Filipino martial arts practitioner and active contributor to various libraries within the React/Redux ecosystem.

Elm is Made of Haskell

Elm’s homepage touts itself as “a delightful language for building reliable webapps,” promising great performance without runtime exceptions. The nearly pure functional language takes most of its syntax and concepts from Haskell and ML, and it aims to marry the best of functional programming with HTML, CSS and JavaScript interoperability and a strong focus on being user friendly and producing clean, concise, expressive code with minimal tooling. Think of it like the Python of pure functional languages. Elm is statically typed via a Hindley-Milner type system and compiles to JavaScript.

Elm takes a rather blatant preference for functional programming:

Why a functional language?

Forget what you have heard about functional programming. Fancy words, weird ideas, bad tooling. Barf. Elm is about:

  • No runtime errors in practice. No null. No undefined is not a function.
  • Friendly error messages that help you add features more quickly.
  • Well-architected code that stays well-architected as your app grows.
  • Automatically enforced semantic versioning for all Elm packages.

Redux, I am Your Father

The Elm architecture inspired the Redux JavaScript library. This pattern has been gaining notice recently as “JavaScript fatigue” reaches an all time high, with more JavaScript developers willing to give non-JavaScript solutions a chance. Since Elm inspired Redux, mandates immutability and declarative programming, and has its own virtual DOM implementation, maybe it isn’t a far cry for those of us who have used the React ecosystem.

Let’s explore what it’s like to work with Elm. How do we handle errors? How do we debug? Is it as beginner-friendly as it claims?

Setting Up

First, I need to get everything installed. After running the Mac installer, I have all the Elm goodies on my PATH: elm-repl, elm-reactor, elm-make, and elm-package. That took all of 3 minutes. No hiccups so far. Following the Elm instructions, I next installed the “elm” extension for VS Code. After installing, I can create an Elm file and get nice syntax highlighting, as shown in their example gif:

Elm Error Highlighting

I also installed the elm-format extension. So far so good! The remainder of the docs cover the Elm language and Elm architecture. I read through these fairly brief overviews, but I won’t rehash them here. Let’s try to get a simple app running.

Hello, Elm

On the command line:

$ mkdir hello-elm
$ cd hello-elm
$ elm-package install elm-lang/html
Some new packages are needed. Here is the upgrade plan.

  Install:
    elm-lang/core 5.0.0
    elm-lang/html 2.0.0
    elm-lang/virtual-dom 2.0.3

Do you approve of this plan? [Y/n] Y
Starting downloads...

  ● elm-lang/virtual-dom 2.0.3
  ● elm-lang/html 2.0.0
  ● elm-lang/core 5.0.0

Packages configured successfully!

Per the docs, elm-reactor is a way to “get a project going quickly.” We should be able to create a Main.elm file, put some code in it, and run elm-reactor in order to build and run the file in the browser. Let’s try that. I opened the “hello-elm” directory we just created in VS Code, and I see an “elm-stuff” directory and an “elm-package.json” file. Now I’ll create a file called “Main.elm” (following Elm file name conventions, the entry module is called “Main” and all modules are capitalized). I’ve added these lines in the file as an initial test:

import Html exposing (..)
main =
  div [] [ text "Hello, Elm!" ]

Now to run it via elm-reactor. On the command line in our project directory:

$ elm-reactor
elm-reactor 0.18.0
Listening on http://localhost:8000

Now we just go to localhost:8000 and click the “Main.elm” link to build and run our module.

Elm Reactor Page

Elm Reactor Page

Elm Hello World

Elm Hello World

Success! So far there’s been absolutely no fuss following the setup and first app instructions.

Using the Elm architecture

Now let’s try the canonical counter example given in the docs. Replace the contents of Main.elm with the following:

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Html.beginnerProgram { model = 0, view = view, update = update }

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

It works!

Elm Page Counter

Elm Page Counter

We now have a working example of the Elm architecture.

Compilation

So far I’ve been impressed with the out of the box features and simplicity of Elm’s tooling, but how does Elm handle compiler errors? Can we sneak anything past the compiler? Let’s give that a shot. I’ll make an intentional mistake in Main.elm and try to use something that is undefined:

    Increment ->
      model + 1 + x

VSCode Elm Error

VSCode Elm Error

First off, notice that the VS Code Elm extension catches this. Awesome! Now let’s see what elm-reactor has to say about it:

Elm Reactor Error

Elm Reactor Error

Not only is Elm refusing to compile this, it gives us some suggestions for a possible fix. The error messaging overall is pretty useful. This is a much needed departure from the cryptically academic error messages of the older functional languages which inspired Elm.

I tried this:

update msg model =
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model.fake - 1

and VS Code Elm extension told me “model does not have a field named ‘fake’.” After working in only JavaScript for the past two years, this seems pretty cool! Lo and behold, the same output is also shown in elm-reactor when we try to run the code:

Elm Reactor Error Fake

Elm Reactor Error Fake

Well, the compiler foiled us at every turn, but one way to crash Elm at runtime is to do so on purpose with the tool they created for this job!

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Html.beginnerProgram { model = 0, view = view, update = update }

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    , div [] [ button [ onClick Crash ] [text "Crash"]]
    ]

type Msg = Increment | Decrement | Crash

update msg model =
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model - 1
    Crash ->
      Debug.crash "Pow!"

This renders the app unresponsive and gives us an uncaught JavaScript runtime exception:

Elm Reactor Crash

Elm Reactor Crash

Debugging Elm

The Elm compiler attempts to remove logical errors in your code, but the app is still going to run on the web (“the most hostile software engineering environment imaginable”). Errors are going to happen from timing failures, incompatible browsers, and invasive plugins. We’re not going to get very far without monitoring and debugging. So how does Elm’s debugger work? Can I trace variables? Can I set breakpoints and inspect? Much to my surprise and disappointment, the official guide has no information about debugging!

However, I did notice when running Elm files via reactor that there’s a built in debugger attached.

Elm Reactor Debugger

Elm Reactor Debugger

This feels very similar to Redux devtools (reverse and replay actions, see the app state after any given action, etc), but, despite its benefits, this tool doesn’t meet my expectations for overall debuggability.

I found very few trustworthy and up to date resources on Elm debugging. I tried piecing together what scant information I could dig up via google, but all the sources seemed to conflict. For example, most of the debugger functions don’t seem to be present in the version of Elm I have installed (0.18.0, the latest official release at the time of writing). Luckily, VS Code saved me.

VSCode Elm Debug

VSCode Elm Debug

So it appears that Debug only has two methods, crash and log, one of which we’ve already used. Let’s try the other one. Gleaning what I can from the method signature, I tried using log like this:

    Increment ->
      Debug.log "model" model + 1

I’m inferring that Debug.log takes one string argument to use in the log output, one argument of any type of value, and then returns the second argument so that the debug expression evaluates to that argument (is replaced by it) making it a no-op in relation to the surrounding code. Let’s see if elm-reactor agrees with me:

Elm Debug Log

Elm Debug Log

It works! Cool.

I’ve been able to fairly easily figure out how to use the native Debug module in Elm thanks VS Code’s Elm integration. However, the lack of documentation and apparent instability of the approach to debugging in Elm is the only pain point I’ve come across so far.

Monitoring Elm

Under the covers, the debug module will expose messages through the console object, where TrackJS will automatically capture them as part of the Telemetry Timeline. Integrating TrackJS monitoring into your Elm app is pretty straightforward. First we need to make an index.html with elm-make:

elm-make Main.elm
Success! Compiled 1 module.
Successfully generated index.html

Taking a look at the output file, we can see some clean HTML and JavaScript. Simply paste the TrackJS installation snippet before the <script> generated by Elm

<!DOCTYPE HTML>
<html><head><meta charset="UTF-8"><title>Main</title><style>html,head,body { padding:0; margin:0; }
body { font-family: calibri, helvetica, arial, sans-serif; }</style>
<!-- BEGIN TRACKJS -->
<script type="text/javascript">window._trackJs = { token: 'your token' }</script>
<script type="text/javascript" src="https://cdn.trackjs.com/releases/current/tracker.js"></script>
<!-- END TRACKJS -->
<script type="text/javascript">
(function() {
  'use strict';

  function F2(fun) {
    function wrapper(a) { return function(b) { return fun(a,b); }; }
    wrapper.arity = 2;
    wrapper.func = fun;
    return wrapper;
  }

  function F3(fun) {
    function wrapper(a) {
      return function(b) { return function(c) { return fun(a, b, c); }; };
    }
    wrapper.arity = 3;
    
  }
})();
</script></body></html>

Tradeoffs of Compilation

Since we’re leaving everything to the compiler, we get no sourcemaps for production debugging. Sourcemaps have been brought up as a feature request for Elm, but they have all been denied per the argument that you only need sourcemaps if you expect runtime exceptions in the compiler output. If an error happens, it’s considered a bug in the compiler and treated as a top priority. This also means the Elm development work cycle is different than the JavaScript one. Instead of the process being: write code, debug in the browser, modify code, repeat; our process is: write code, compile, modify code per any compiler errors, repeat.

Compiler errors are surprisingly useful in Elm, however, if for any reason we did need to debug the user experience of our app, we would be stranded without sourcemaps. In this sense, using Elm instead of JavaScript means trading runtime debuggability for the compiler’s guarantee of no runtime exceptions.

This is the web, so debugging continues to be absolutely necessary. Luckily, debugging Elm’s compiled JavaScript at runtime is fairly straightforward.

Elm Production Debugging

Elm Production Debugging

Debugging is also a desirable tool for development in Elm when our code compiles, but does not behave as expected. For example, bottomless recursion will not cause a compiler error:

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Html.beginnerProgram { model = 0, view = view, update = update }

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      update msg model

This will cause the JavaScript thread to lock up, making the app unresponsive.

Wrapping Up

In this post, we became acquainted with the reasoning behind Elm, used its built-in tools and development flow, tried out the Elm architecture, built a trivial frontend web application in Elm, and learned to do some basic monitoring and debugging. If you’re building in Elm, or any other JavaScript tool, be sure to check out TrackJS so you know when it breaks! Grab a free 30 day trial today.