Elm Functional Programming – Benefits of No Runtime Errors

elmfunctional programmingjavascriptweb-development

Some languages claim to have "no runtime errors" as a clear advantage over other languages that has them.

I am confused on the matter.

Runtime error is just a tool, as far as I know, and when used well:

  • you can communicate "dirty" states (throwing at unexpected data)
  • by adding stack you can point to the chain of error
  • you can distinguish between clutter (e.g. returning an empty value on invalid input) and unsafe usage that needs attention of a developer (e.g. throwing exception on invalid input)
  • you can add detail to your error with the exception message providing further helpful details helping debugging efforts (theoretically)

On the other hand I find really hard to debug a software that "swallows" error. E.g.

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

So the question is: what is the strong, theoretical advantage of having "no runtime errors"?


Example

https://guide.elm-lang.org/

No runtime errors in practice. No null. No undefined is not a function.

Best Answer

Exceptions have extremely limiting semantics. They must be handled exactly where they are thrown, or in the direct call stack upwards, and there is no indication to the programmer at compile time if you forget to do so.

Contrast this with Elm where errors are encoded as Results or Maybes, which are both values. That means you get a compiler error if you don't handle the error. You can store them in a variable or even a collection to defer their handling to a convenient time. You can create a function to handle the errors in an application-specific manner instead of repeating very similar try-catch blocks all over the place. You can chain them into a computation that succeeds only if all its parts succeeds, and they don't have to be crammed into one try block. You are not limited by the built-in syntax.

This is nothing like "swallowing exceptions." It's making error conditions explicit in the type system and providing much more flexible alternative semantics to handle them.

Consider the following example. You can paste this into http://elm-lang.org/try if you would like to see it in action.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Note the String.toInt in the calculate function has the possibility of failing. In Java, this has the potential to throw a runtime exception. As it reads user input, it has a fairly good chance of it. Elm instead forces me to deal with it by returning a Result, but notice I don't have to deal with it right away. I can double the input and convert it to a string, then check for bad input in the getDefault function. This place is much better suited for the check than either the point where the error occurred or upwards in the call stack.

The way the compiler forces our hand is also much finer-grained than Java's checked exceptions. You need to use a very specific function like Result.withDefault to extract the value you want. While technically you could abuse that sort of mechanism, there isn't much point. Since you can defer the decision until you know a good default/error message to put, there's no reason not to use it.