Error Handling in Elm

This came up on Slack, and I found myself thinking about today, so here goes…

What are the best practices for error handling in Elm? - A language that does not have exceptions, which are a tool that a lot of more common languages have and that most Elmers are also likely to be aware of.

My thoughts; feel free to criticise, correct or share your understanding.

How do we represent errors in Elm?

What type?

There are 2 obvious ways, Maybe or Result. I prefer to think of Maybe as for things that are optional. Result is a better way of representing an error, because we can give an error message or other context to the error.

head : List a -> Maybe a

Is it really an error for a list to not have a head? It can be if we are expecting that to be the case, but then we should use a NonEmptyList. Also, in this case there can only be one way things can go ‘wrong’, so the error should be obvious, and a Maybe is sufficient to cover that. head is also a better seen as a partial function that something that can produce an error.

What type arguments?

We could define an error by an error message which is a String, or we could provide some more structured data. Which is more appropriate in different situations?

Result String a

A convenient way of expression an error is to just describe in a String what went wrong, and wrap that as a Result.

This seems most appropriate to me as a way of reporting an error to the developers than to the end user. End users might want error descriptions to be translated into their own language.

This also does not seem appropriate in situations where a recovery might be possible. The code might need information about what went wrong in a more structured way in order to attempt a recovery.

For example, the Http package has:

type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Int
    | BadBody String

If we got a Timeout error, we could recover by trying again automatically. That would be trickier to figure out from a String error message.

I can see a parallel with runtime and checked exceptions in Java here. A runtime exception represents a bug in Java (like division by zero), and is something the developers want to find out about in order to fix the bug. A checked exception is either something that might have a recovery, or some appliciation logic level thing representing an error the application user should be told about. But Elm does not have runtime exceptions, I hear you say.

Recently I was using an API, that I had generated a stub from its API specification. The return type was something like this:

{ accessToken : Maybe String,
  idToken : Maybe String,
  authenticated : Bool,
  expiry : Maybe Int 
}

I know that when the flag is True all the Maybes will be Just, but the return type does not give me that. Nor could the stub generator infer it, because its information that was simply missing from the spec used as a source for the stub. In my code I have to assume they will all be Just but the compiler forces me to deal with the alternatives that ‘should-never-happen’. I feel confident it won’t happen and if it does the end user is not interested in being told - it is for the technical team to fix investigate and fix that bug. So that one seems to operate a lot like a runtime error, even though it is not in Elm.

As a custom type, instead of Result.

Seems to me that application level ‘error’ conditions, in which the user might very well be interested, might be better expressed with custom types. Suppose you have some web application that monitors some data that you must check every day, but today the data has failed to arrive. The user wants to know when the data is not available, so they can phone the ops team and ask what the hold-up is:

type DailyData = 
     Data DataModel
   | NoData

The user will be told about this by rendering different views from this data model. It seems a bit more direct and less inconvenient than using Result - I won’t make use of functions like Result.mapError on these errors, they will drive view logic more directly; so is it worth the extra layer of wrapping in a Result?

More…

Next time I feel like adding to this, where can errors come from in Elm programs?

8 Likes

Did you have a look at RemoteData? That’s pretty much the standard I see in Elm packages.

2 Likes

Lists can be empty so List.head needs to provide some value if they are. A NonEmptyList is only useful if you know that your list could never need to be empty, but it’s common to need to possibility of an empty list and handle that case when it comes up.

In this situation the error is at the point where you decode the data from the API. If it’s invalid then you fail the decoder, show a request error to the user and never have to deal with the Maybes at all. If you need to be able to handle this kind of structure in Elm then a custom type is preferable to multiple Maybe fields with an implied relationship.

Providing an error as a String isn’t great, but it’s often the only option because you often only get string errors from various APIs or the error is too complicated to usefully describe in a value. A more structured error like what the Http module has is preferred when possible.

Yes, it’s common for application specific error states to be represented with a custom type. Result is useful when you’re dealing with generic code that doesn’t need to know about your application specific error handling. Result also provides a lot of useful functions for combining error and chaining functions that may error.

Maybe is for contexts where there is only one reason for the failure. Result is for the case where there are multiple reasons. The error type captures these reasons and it can be as simple as a String or as complex as a Http.Error. It really is tied into how the user of the module wants to use the error. Maybe the user needs complex logic to deal with each individual error or maybe they just want a string representation of the error to send to some logger. As a creator of a library or module, giving the user a custom type gives them a lot more freedom.

This is a typical case of impedance mismatch. If you receive a JSON like that you don’t need to have your Elm type mirror it. You can use a sophisticated decoder that decodes that JSON into a sensible Elm custom type that captures the state reasonably.

I would write this as type alias DailyData = Maybe DataModel. I’m sure, someone else might prefer custom types but if the semantics of a datatype matches perfectly on one of the core types, I prefer to use the core type. In this case, the semantics match perfectly on Maybe.

The same with booleans. Some people prefer custom types for boolean values. I would rather prefer to name the fields in such a way as to use a boolean.

So, instead of type DoorState = Open | Closed and a field called doorState I would prefer isDoorOpen and just use a Bool.

@pdamoc nd @jessta

About the decoding of some back-end response, example. Its a good point about my decoder not being optimal - I need a better example because that distracted from what I was really trying to get accross.

Suppose instead, I decoded into this structure:

type alias Response = 
    Maybe 
        { accessToken : String
        , idToken : String
        , expiry : Int 
        }

Much nicer to work with in the Elm code that will do stuff with this response.

However, from an error handling perspective, there is still the error that the decoder might fail. If the back-end does something unexpected and only gives me some of the fields. In this case, the back-end spec and docs were not really clear whether that could happen or not, since they only said all the fields were optional, and did not group them under a Maybe like in the cleaner structure above. Having used this API for a while, I feel fairly confident that won’t happen, but I’m definitely not 100% certain given the quality of the docs.

What I was trying to get accross is the question of - who is interested in this error? Is it the development team, the end user, or the code itself since there might be some automatic recovery mechanism it can implement.

In this case, I think it is the development team who are most interested in the error. Since they introduced a bug by assuming that its either all or nothing with this set of fields. The user will find out about the error too - in this case it will probably manifest in the application by the user being unable to log in. If we can get something back to the development team in a log that says “idToken missing from response”, that will let them know something is up with this application, and guide them to the place in the code where they need to go to fix it.

Mapping different kinds of errors onto the audience most interested in knowing about them, yields an approximate set of rules like this:

  • Development team; unexpected states in the code; a unique string describing the problem that is created near the site of the error detection so they can grep for it in the code.

  • End user; errors relating to the application logic; consider mapping onto a custom type describing the application state, and build a nice view to explain what is wrong.

  • The code itself; anticipated error states; can create a custom type describing the possible errors and give enough information to recover from them automatically where possible. If no recovery is possible, may toString the error and pass it up for logging.

For the scenario that you described it is the development team. For this kind of scenarios, I usually, log the error to Rollbar and inform the user that something unexpected happened. “There was a problem trying to {insert action here} please try again later.” kind of of message.

A change in api will trigger a BadPayload error if the decoder is not able to handle the change. Usually, changes in the API should be documented in some kind of API semver scheme that should let the API user know what to expect.

Never tried Rollbar. How do you interact with it from Elm? Is it through a port, or does it have an HTTP API you can use more directly?

I use it through a port.

Where can Error come from?

I wanted to have a think through the what parts of an Elm program are likely to be sources of errors.

Looking at an Elm program type:

document :
    { init : flags -> ( model, Cmd msg )
    , view : model -> Document msg
    , update : msg -> model -> ( model, Cmd msg )
    , subscriptions : model -> Sub msg
    }
    -> Program flags model msg

You might have this program type at the top-level but smaller parts of the program may have their own init, view, update or subscription functions that handle some part of the overall program, and you have more freedom in how those are structured, you just need to arrive back at the top with the correct overall TEA type for the program. This sounds like nested TEA, but even if you have a flat program structure, these functions might call helper functions in each of those areas.

init Can produce errors. Suppose I have part of a program that needs to be initialized with the URL of some back-end service it will talk to. It expects a valid URL in the config. I may take a config parameter in as a String, and sanity check that it parses to a URL before building a model for this part of the program and returning it. So this init function could have the type:

init : { apiEndpoint : String } -> Result String Model

There could be lots of other ways in which a config is parsed to determine its validity, producing errors. If init fails due to bad config, its probably the development team that want to know and get this error logged.

view Should most likely not be allowed to produce any errors. I think if a view function produces errors it points to a bad model. There is going to be a way of restructuring the model so that every possible model state results in at least some view - even if its just an empty div. I sometimes put in an empty div during development, when I have some model state not quite fully worked out yet. There might be some error state in the model for errors which should be reported to the end-user too, and those will turn into part of the view that tries to give a helpful error message.

update Can produce errors. Since update produces side-effects that interact with the world outside of the Elm program, that can be a source of errors - for example, back-end not reachable due to no net connection. If an error can be recovered from, it will most likely be best to do that in the same update that triggered the side-effect that caused the error in the first place, as that is where the context of the error will be easiest to understand. In that case, the error won’t leak out of the update function.

On the other hand, if an update causes an error that cannot be recovered from, it could be returned from that update function:

update : Msg -> Msg -> Result String (Model, Cmd Msg)

In this type, when an error occurs, the model is not updated, and no side-effects are produced. The caller gets an Err "message". This is a lot like an exception.

subscriptions Similar argument to view, it should not produce errors. If it does, it suggests the model needs some re-thinking.

2 Likes

What if there is more than one error?

Examples

Here are some examples of situations in which there could be more than one error:

  • A user filling in a form, with multiple input elements to be completed, and those inputs need to be validated against being not-empty, matching some regex, and so on. There could be multiple errors on a single form, all of which need to be reported back to the user.

  • Parsing something, markdown, json, code, whatever. Its not so nice if parsing just dies at the first error. If possible, it should keep going and report back multiple errors in one go.

  • Building several things through ‘smart constructors’ in order to create a valid input for some function. A ‘smart constructor’ or ‘refined type’ is one where you do not call the constructor directly, but have a function that can return a Result or Maybe to check the value given. For example:

type MoreThanOne
    = MoreThanOne Int

moreThanOne : Int -> Maybe MoreThanOne
moreThanOne x = 
    if x > 1 then
        MoreThanOne x
    else
        Nothing

Supposing you need to build several of these, perhaps to create a valid Config to create some part of your program, or perhaps to create a valid input to pass to some HTTP API, and so on. Its nice to check inputs are valid, but its a pain to deal with the errors that can result from that checking.

Representation

One way of representing multiple errors is like this:

import List.Nonempty exposing (Nonempty)

someFunReturningMultipleErrors : a -> Result (Nonempty err) val

I used Nonempty here, because you don’t want to get an Err [], as that is saying there was an error but there wasn’t one.

Gathering multiple errors into this representation.

For a single error, we can just use Result.map, its more complex for the multiple errors case.

What if I need to call several functions that can return errors and combine their errors together when one or both of them produces an error? There are 4 possibilities here, for 2 pairs of possibilities of Ok or Err. Its a pain to write out a 4-way case statement each time you encounter this, so I write some helper functions like this:

map2ResultErr : (a -> b -> c) -> Result (Nonempty err) a -> Result (Nonempty err) b -> Result (Nonempty err) c
map2ResultErr fun first second =
    case ( first, second ) of
        ( Ok checkedArg, Ok checkedRes ) ->
            fun checkedArg checkedRes |> Ok

        ( Err error, Ok _ ) ->
            Err error

        ( Ok _, Err error ) ->
            Err error

        ( Err error1, Err error2 ) ->
            List.Nonempty.append error1 error2
                |> Err

and similarly for map3. To get higher map numbers, cannot use a flat case statement since Elm no longer supports tuples larger than 3, so the case statements need to be nested.

I couldn’t think of a better name for this, its name is supposed to reflect that its a map over a result with possibly multiple errors.

I also have this version for working with multiple results each of which can have errors. The idea is that if any of the results contains errors, the overall result will be an error that gathers up all of the errors accross all of the results. If none of the results have errors, the result is the same as doing a List.map over the list of valid values.

mapResultErrList : (a -> b) -> List (Result (Nonempty err) a) -> Result (Nonempty err) (List b)
mapResultErrList fun results =
    List.foldl
        (\result accumRes ->
            case ( result, accumRes ) of
                ( Ok val, Ok accum ) ->
                    fun val :: accum |> Ok

                ( Err err, Ok _ ) ->
                    Err err

                ( Ok _, Err errAccum ) ->
                    Err errAccum

                ( Err err, Err errAccum ) ->
                    List.Nonempty.append err errAccum |> Err
        )
        (Ok [])
        results
1 Like

I think I could make a module out of the above with helper functions for the multiple error cases. Prefixing with a module name should help with the naming too. Something like:

module MultiError exposing (..)

import List.Nonempty exposing (Nonempty)

type alias MultiError = Result (Nonempty err) val

{-| Was map2ResultErr -}
combine2 : (a -> b -> c) -> MultiError err a -> MultiError err b -> MultiError err c

combine3 
... and so on

{-| Was mapResultErrList, dropped the mapping function bit too. -}
combineList : List (MultiError err a) -> MultiError err (List a)

combineDict
combineSet
... and so on

For the case where you just want to report 1 error, the first one encountered, I notice that elm-community/result-extra already has:

https://package.elm-lang.org/packages/elm-community/result-extra/latest/Result-Extra#combine

combine : List (Result x a) -> Result x (List a)
1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.