HTTP Error Checking in Elm
Pattern Matching is Your Friend
One area I’ve been glossing over as I’ve been working with HTTP in Elm is error checking. For the sample projects, I’ve either been ignoring errors or blindly converting any errors to a string and storing them in my model. I wanted to tae some time to explore how to use Elm pattern matching to more easily get the detailed error information and explore what error information is provided by the HTTP package.
The Starting Point
In my previous post on Elm and HTTP, I ended up with this update function.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateWeather result ->
case result of
Ok weather ->
{ model | temperature = weather.tempF } ! []
Err error ->
{ model | error = toString error } ! []
Here we have one message type UpdateWeather that receives a Result type and processes it and updates either the weather result or the error field depending upon the presence of an error.
Simplifying with Pattern Matching
Looking at the code above, it seems overly complex and the nested case statement doesn’t seem to pass the “smell test.” Coming from a non-functional background, my first instinct was to factor the inner case statement into its own function. It turns out that we can use the power of Elm’s pattern matching syntax to simplify it without creating a new function.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateWeather (Ok weather) ->
{ model | temperature = weather.tempF } ! []
UpdateWeather (Err error) ->
{ model | error = toString error } ! []
Here we augment the outer case statement to match on the UpdateWeather function twice, with two different patterns matches. The first matches the Ok condition and the second matches the Err condition. While it’s not a huge reduction in code, it does make for much more readable and satisfying code.
Next, we’ll dive into how to extract more information from the error case.
What Does an Error Return?
Digging into the Elm documentation, we see that the Http.send function has this signature.
send : (Result Error a -> msg) -> Request a -> Cmd msg
This means that our update function is dealing with functions that convert Result Error a to Msg types. So in the case of an error, we will get the Error type returned. Error is defined like this.
type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus (Response String)
| BadPayload String (Response String)
and uses the Response type which is defined in this way.
type alias Response body =
{ url : String
, status : { code : Int, message : String }
, headers : Dict String String
, body : body
}
This will give use complete access to the data returned by the call. In my previous example, we have been depending on the toString function to convert the Error data into something we can save. Now that we know about the underlying data format, we can do something slightly more information.
Extracting the Error Info
Armed with our knowledge of pattern matching, it might be tempting to expand our update function to pattern match on each error type defined in the Error type. This would give us an update function that looks like this.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateWeather (Ok weather) ->
{ model | temperature = weather.tempF } ! []
UpdateWeather (Err (BadUrl message)) ->
{ model | error = toString message } ! []
UpdateWeather (Err Timeout) ->
model ! []
UpdateWeather (Err NetworkError) ->
model ! []
UpdateWeather (Err (BadStatus response)) ->
model ! []
UpdateWeather (Err (BadPayload message response)) ->
model ! []
I didn’t bother to fill in the action in each case because this isn’t going to be my final solution. I found this really intellectually satisfying in showing the power of nested pattern matching functions. But in the end, cluttering up the update function with six cases for one HTTP call seemed as big a “code smell” as the previous nested case.
This seemed to be the perfect case to extract a function.
httpErrorString : Error -> String
httpErrorString error =
case error of
BadUrl text ->
"Bad Url: " ++ text
Timeout ->
"Http Timeout"
NetworkError ->
"Network Error"
BadStatus response ->
"Bad Http Status: " ++ toString response.status.code
BadPayload message response ->
"Bad Http Payload: "
++ toString message
++ " ("
++ toString response.status.code
++ ")"
In this case, I simply did my own string conversion but given that you now have full access to the response, you could do whatever made sense. I suspect in most cases, you’d want to interface to whatever error logging package you use for production error logging.
With this function factored out, we are back to a simple and elegant update function.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateWeather (Ok weather) ->
{ model | temperature = weather.tempF } ! []
UpdateWeather (Err error) ->
{ model | error = httpErrorString error } ! []
Summary
There’s not a whole lot of magic in this post. I just know that after struggling to get JSON decoding and HTTP access working for the first time, it’s sometimes a challenge to go back and put all the pieces together, especially in the non-happy path case. But, the beauty of Elm is that it’s extremely easy to dig into the documentation and source code and to understand the underlying data structures of the libraries. This knowledge can be used to streamline and simplify your code. In the end, making good use of Elm’s pattern matching capabilities and with judicious extraction of functions, you can end up with simple, elegant and reusable code.