Elm Cmd vs Task: Choosing the Right Tool for Side-Effects
Elm side-effects: use Cmd for simple one-shot effects and Task for composable, error-handling operations. A decision guide with a concrete HTTP example.
30 Sept 2025, 05:13 UTC

You need to fetch JSON, read the clock, or generate a random value in an Elm app. The compiler forces a decision: does this effect belong in a Cmd or a Task? The short answer: use Cmd for simple, fire-and-forget effects and Task when you need composition, error handling, or chaining. This guide assumes Elm 0.19.1 with the elm/http package installed.
The decision and its constraints
In the Model-Update-View (MVU) architecture, update must return (Model, Cmd msg) — a tuple of the new model and a command. Both Cmd and Task describe effects, but they differ in how much structure they give you.
| Option | What it is | Best for | Cost |
|---|---|---|---|
Cmd | A single, opaque effect value | Random, time, ports, simple one-shot actions | No built-in composition or error handling |
Task | A chainable effect with success/failure | HTTP calls, sequences, retries | Must be converted to Cmd via Task.perform |
Trade-offs
Cmd is concise. Random.generate, Time.now, and port commands all produce a Cmd msg directly. There is no way to attach an error handler or chain one effect after another — if the effect can fail, you handle failure in the message itself.
Task is a value that describes a computation that may succeed with an a or fail with an x. You compose tasks with Task.map and Task.andThen, so you can sequence HTTP calls, recover from errors with Task.onError, and build retry logic in a testable way. The price is ceremony: the runtime does not execute a Task directly. You must convert it into a Cmd msg with Task.perform, which takes a function from the task's success value to a message. Forgetting this step is a type mismatch the compiler will catch.
Concrete implementation: fetching JSON
The example below fetches a string from an HTTP endpoint and delivers the result as a message. Run elm make Main.elm from the project directory (no special permissions needed) to check it compiles.
import Http
import Task
type Msg
= GotText (Result Http.Error String)
fetchText : Cmd Msg
fetchText =
Http.getString "https://example.com/data.txt"
|> Http.toTask
|> Task.attempt GotText
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotText result ->
case result of
Ok text ->
( { model | content = text }, Cmd.none )
Err _ ->
( model, Cmd.none )
Here Http.getString builds a Task (in elm/http 2.x; in earlier versions it produced a Cmd directly). Task.attempt converts the task into a command that sends GotText with the result. If you need to chain a second request after this one, Task.andThen lets you do it inside the task, before the single Task.attempt at the end.
When to prefer Cmd
For Random.generate, Time.now, or a port command, a Cmd is the only option — these effects have no failure mode to model. Reach for Task only when the effect can fail or must be sequenced.
Checking the result
Compile with elm make Main.elm; a type error here usually means a missing Task.attempt or a Task that never became a Cmd. At runtime, open the browser console and confirm the network request fires and the JSON decodes. If you see a type mismatch about Cmd msg, check that update returns a tuple with Cmd.none in the branches that don't trigger effects.
Limitations
In Elm 0.19.0, Http.getString returned a Cmd; the Task-based API shown requires elm/http 2.x. Task.attempt is the standard way to convert a task into a command, but you can also use Task.perform when the task cannot fail. Cancellation is not built-in — you would need to model it yourself with a flag in the model.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.