Elm’s Http Module: Typed Requests, Result‑Based Error Handling, and a Safe JSON Fetch Example
Elm’s built‑in Http module turns side effects into Cmd messages and forces explicit Result error handling. This blog walks through a safe JSON fetch example, decoder, request, error handling, and trade‑offs.
08 Sept 2026, 20:50 UTC

Why Typed HTTP Requests Matter in Elm
When you build an Elm app that talks to a REST API, you usually start with a button that says “Load data.” Behind the scenes you want to fire a GET request, parse JSON, and render the result. In many languages you can do this with a single line of code, but you end up trusting the server to send back the shape you expect. Elm’s elm/http package flips that paradigm: it forces you to describe the request and the JSON shape up front, and the compiler guarantees that the data you handle later matches what you declared.
The core idea is simple: side effects live in Cmds, and the only way to get the response back is through a Result type that you must pattern‑match on in update. This eliminates a whole class of runtime bugs that come from mismatched JSON or unhandled errors.
Getting Started – A Minimal Elm Project
- Run
elm initto create a new project. - Add the HTTP library:
elm install elm/http. - Create
src/Main.elmwith the following skeleton.module Main exposing (main) import Browser import Html exposing (Html, button, div, text) import Html.Attributes exposing (class) import Html.Events exposing (onClick) import Http import Json.Decode as Decode -- MODEL ------------------------------------------------------------ type alias Model = { data : Maybe MyData , error : Maybe String } initialModel : Model initialModel = { data = Nothing, error = Nothing } -- UPDATE ----------------------------------------------------------- type Msg = Fetch | GotResponse (Result Http.Error MyData) update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of Fetch -> (model, fetchData) GotResponse result -> case result of Ok d -> ({ model | data = Just d, error = Nothing }, Cmd.none) Err e -> ({ model | data = Nothing, error = Just (errorToString e) }, Cmd.none) -- VIEW ------------------------------------------------------------- view : Model -> Html Msg view model = div [] [ button [ onClick Fetch ] [ text "Load data" ] , case model.data of Just d -> div [] [ text ("Name: " ++ d.name) ] Nothing -> text "" , case model.error of Just err -> div [ class "error" ] [ text err ] Nothing -> text "" ] -- MAIN ------------------------------------------------------------ main : Program () Model Msg main = Browser.sandbox { init = initialModel, update = update, view = view } - Run with
elm reactorand navigate tohttp://localhost:8000.
Defining the JSON Decoder
Elm’s Json.Decode module turns raw JSON into typed values. The compiler checks that your decoder matches the type you specify in Http.expectJson. If you change the decoder but forget to update the type, you’ll get a compile‑time error.
type alias MyData =
{ id : Int
, name : String
}
myDataDecoder : Decode.Decoder MyData
myDataDecoder =
Decode.map2 MyData
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
Building the Request
The Http.get function builds a request that returns a Result containing either an Http.Error or your decoded value. Notice the type annotation: Http.get : String -> Decoder a -> Request a. The type variable a is inferred from the decoder.
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/users/1"
, expect = Http.expectJson GotResponse myDataDecoder
}
Because fetchData is a Cmd, it must be returned from update when the Fetch message is received.
Handling Errors Explicitly
Elm’s Result type forces you to consider both success and failure. The Http.Error enum includes BadUrl, Timeout, NetworkError, and BadStatus (with the HTTP status code). In the example above, GotResponse is the message that carries the Result. The update function pattern‑matches on it and updates the model accordingly.
errorToString : Http.Error -> String
errorToString err =
case err of
Http.BadUrl url -> "Bad URL: " ++ url
Http.Timeout -> "Request timed out"
Http.NetworkError -> "Network error"
Http.BadStatus status -> "Bad status: " ++ String.fromInt status
Http.BadBody msg -> "Bad body: " ++ msg
Trade‑Off: No Built‑In Retry or CORS Handling
While Elm guarantees type safety, it does not provide retry logic out of the box. If you need exponential back‑off or a limited number of retries, you’ll have to write a small helper that re‑dispatches Fetch after a delay. Also, because Elm runs in the browser, cross‑origin requests must be allowed by the server. If the server does not send Access‑Control-Allow-Origin, the request will fail before Elm sees it, and you’ll receive a NetworkError.
Practical Checklist
- Verify the API endpoint is reachable and returns the expected JSON.
- Run
elm make src/Main.elm --output=main.jsto ensure no type errors. - Open the browser console and network tab to confirm the GET request is sent with the correct URL.
- Check that the UI displays the data on success and shows a friendly error message on failure.
- Test error handling by pointing the URL to an invalid endpoint or altering the decoder to mismatch the JSON; the compiler should flag the mismatch or you should see a
BadBodymessage.
Next Steps
Once you’re comfortable with the basics, consider these extensions:
- Use
Http.postfor sending JSON bodies, leveragingEncode.objectto build the payload. - Implement a small retry helper that dispatches
Fetchagain after aTask.succeed()delay. - Integrate with a state‑machine in the Elm Architecture to manage loading, success, and error states more cleanly.
Elm’s elm/http module may look like a small wrapper, but its type‑safety guarantees make it a powerful tool for building robust front‑end applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.