Commands: Effects as Data
The counter’s update returned ( newModel, Cmd.none ) and we ignored the second slot. That slot is how a pure function causes things to happen in the world. This chapter makes the frontend’s effect story concrete; it is the “effects are values” principle from Part I, wearing its work clothes.
The contract
update : Msg -> Model -> (Model, Cmd Msg)
update answers two questions at once: what is the new state and what should the runtime do next. A Cmd Msg is a description of work whose eventual outcome will come back as, precisely, another Msg. The circle stays closed: effects do not interrupt the loop, they travel around it.
The vocabulary is small:
Cmd.none : Cmd msg -- nothing to do
Cmd.batch : List (Cmd msg) -> Cmd msg -- several at once
Cmd.perform : (a -> msg) -> Task a -> Cmd msg -- run a Task, box the result as a Msg
Service.call : Service req resp
-> req
-> (Result Service.Error resp -> msg)
-> Cmd msg -- call the backend
Nav.pushTo : String -> Cmd msg -- navigate
The fetch, in full
Here is the canonical shape: a page that loads its data on entry.
type alias Model =
{ tasks : List Shared.Task
, loading : Bool
}
type Msg
= Fetched (Result Service.Error (List Shared.Task))
init : (Model, Cmd Msg)
init =
( { tasks = [], loading = True }
, Service.call Shared.listTasks () Fetched
)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Fetched (Ok tasks) ->
( { model | tasks = tasks, loading = False }, Cmd.none )
Fetched (Err why) ->
( { model | loading = False }, Cmd.none )
Read init closely, because the timing is the whole lesson. It returns instantly with an empty, loading Model and a command. No data has been fetched when init returns; the page renders its loading state first. The runtime then performs the request, and some milliseconds later update receives Fetched (Ok ...) like any other event. Loading is not a special mode bolted onto the framework; it is simply a Model your view knows how to draw.
Notice also what the compiler is holding for you:
Fetchedcarries aResult, so the failure branch must exist or thecaseis inexhaustive. You cannot forget the error path.- The response type comes from the
Shared.listTaskscontract. If the backend changes what it returns, this page stops compiling.
Why update cannot “just fetch”
The question everyone asks: why the detour? Why can’t update call the server and use the response right there?
Because then update would not be a function anymore. Its result would depend on the network, the server’s mood, and the time of day. Every guarantee from Part I unravels at once: testing needs mocks, replay becomes impossible, the time-travel debugger lies, and “what happened?” is no longer answerable from the message log.
The Cmd detour keeps the boundary crisp. The decision to fetch is pure and lives in your code. The fetching is impure and lives in the runtime. The result re-enters as data. You can hold the entire behavior of a page in your head, or in a test, by listing its messages, and each round through the loop is one atomic step: message in, state plus intentions out.
There is a subtler payoff. Because effects are inert values, update composes them freely: return them from case branches, build them conditionally, batch them. “Save, then navigate, then refresh the list” is not callback choreography; it is a value:
Saved (Ok note) ->
( { model | saving = False }
, Cmd.batch
[ Nav.replaceTo (Frontend.Routes.noteDetail note.id)
, Service.call Shared.listNotes () Refreshed
]
)
Tasks reach the frontend through Cmd.perform
Some effects produce a plain value rather than a service response; the clock is the everyday example. Those are Tasks (the backend chapter covers them fully), and Cmd.perform is the bridge:
init =
( { today = Nothing }
, Cmd.perform GotToday Time.now
)
“Run Time.now; deliver its value wrapped in GotToday.” The pattern is always the same: the Model holds a Maybe Time, renders a placeholder until the message lands, then renders the real thing. One rule of thumb falls out: a Mar frontend never blocks. Anything that takes time arrives as a message when it is ready, and the Model always has a face to show meanwhile.
The discipline, stated once
A page’s behavior is fully specified by three declarations: what can happen (Msg), how state responds (update), and what the world is asked to do (Cmd values). Nothing else moves. When a bug appears, there are exactly three questions, asked in order: did the right message fire, did update map it to the right state, did the view draw that state correctly. Twenty years of “who mutated what, when, from which callback” collapses into that checklist.
Commands cover effects you initiate. The world also volunteers information on its own schedule: clocks tick, keys go down. That is subscriptions.