Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Language Atlas (mini-courses for more languages)  Module 12 – Elm and The Elm Architecture

The Elm Architecture, JSON and ports

Build Elm programs from Model, update, view, Cmd and Sub, decode JSON with errors that name the bad field, and pass data to and from JavaScript safely.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Node.js 24.21.0, quickjs 0.32.0 and Elm 0.19.3
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build a program from a Model, a Msg type, update and view, and say what the runtime does with each
  • Explain what a Cmd and a Sub are, and why Cmd.batch gives no order
  • Write JSON decoders and an encoder with elm/json, and read the path in a decoding error
  • Pass data in with flags and through ports, and decode every value that crosses from JavaScript
  • Prevent text from users from becoming markup on either side of a port, with Html.text and textContent

Before you start

What you’ll learn · Before you start

What you will learn

  • Build a program from a Model, a Msg type, update and view, and say what the runtime does with each
  • Explain what a Cmd and a Sub are, and why Cmd.batch gives no order
  • Write JSON decoders and an encoder with elm/json, and read the path in a decoding error
  • Pass data in with flags and through ports, and decode every value that crosses from JavaScript
  • Prevent text from users from becoming markup on either side of a port, with Html.text and textContent

Before you start

On this page

A server sends "paise": "1450" where the app expects a number. In this lesson’s JavaScript run, the same order still totalled 6750, because * turned the text into a number, while a sum of its prices came out as 12001450; the Elm decoder stopped at the border instead and said where: json.items[1].paise, Expecting an INT.

Here you build programs the way Elm structures every one, a model, messages, update and view, and see what the runtime does between them; you send commands and find that a batch of them keeps no order; you decode and encode JSON with errors that name the bad field; you pass values in through flags and ports and see what happens when one has the wrong type; and you keep text from users from turning into markup on either side of the border.

The runtime calls update and view, and your code returns values

In The Elm Architecture a program is four pieces. The model holds the whole state. A message type, Msg, lists everything that can happen. update takes a message and the model and returns the next model. view takes the model and returns a description of the page. Counter.elm counts cups of chai with three buttons:

Counter.elm: a model, three messages, update and view ELM · tea/src/Counter.elm
module Counter exposing (Model, Msg(..), init, main, update, view)

{-| A counter of cups of chai, as a Browser.element: The Elm Architecture in
one module.
-}

import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)


type alias Model =
    { cups : Int }


type Msg
    = Add
    | Remove
    | Reset


init : () -> ( Model, Cmd Msg )
init _ =
    ( { cups = 0 }, Cmd.none )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Add ->
            ( { model | cups = model.cups + 1 }, Cmd.none )

        Remove ->
            ( { model | cups = max 0 (model.cups - 1) }, Cmd.none )

        Reset ->
            ( { model | cups = 0 }, Cmd.none )


view : Model -> Html Msg
view model =
    div []
        [ p [] [ text (cupsOfChai model.cups) ]
        , button [ onClick Remove ] [ text "Remove one" ]
        , button [ onClick Add ] [ text "Add one" ]
        , button [ onClick Reset ] [ text "Reset" ]
        ]


cupsOfChai : Int -> String
cupsOfChai cups =
    if cups == 1 then
        "1 cup of chai"

    else
        String.fromInt cups ++ " cups of chai"


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

Your code never changes the page and never stores the model: it returns values, and the runtime does the rest. Step through one click:

Interactive · message by message One click through The Elm Architecture Step through a click on Add one, from the button to the text on the page that changes.
One click through The Elm Architecture, step 7 of 7. Messages so far: Elm runtime to The page: view { cups = 0 }; The page to Elm runtime: Add; Elm runtime to Counter: update Add model; Counter to Elm runtime: { cups = 1 }, Cmd.none; Elm runtime to Counter: view { cups = 1 }; Elm runtime to The page: one text node changes.The pagea person clicksElm runtimefrom elm/browserCounterupdate and viewview { cups = 0 }Addupdate Add model{ cups = 1 }, Cmd.noneview { cups = 1 }one text node changeswaits for a message

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The first view. At start the runtime calls init, gets { cups = 0 }, calls view with it and draws the result: "0 cups of chai" and three buttons.
  2. A click becomes a message. The Add one button was drawn with onClick Add. A click on it hands the runtime the message Add; nothing in your code runs yet.
  3. update decides. The runtime calls update Add with the current model. update is a pure function: it returns the new model { cups = 1 } and Cmd.none, a request for nothing.
  4. A new model. The runtime keeps the new model. Your code never stores it anywhere itself, so there is one source of truth for the whole element.
  5. view describes the page. The runtime calls view with { cups = 1 }. view returns a description of the page as Html values, not changes to make.
  6. Only the difference. The runtime compares the new description with the last one and changes the page where they differ: the text becomes "1 cup of chai", and the buttons stay as they are.
  7. Waiting again. The loop is back where it started, waiting for the next message. Replay.elm runs the same update without a page: Add, Add, Remove, Reset, Remove gave 1, 2, 1, 0 and 0.

Because update is a plain function, a program can drive it without a browser. Replay.elm imports Counter and feeds its update five messages, as five clicks would, and the session also builds Counter itself:

Replay.elm: five clicks played through Counter.update ELM · tea/src/Replay.elm
port module Replay exposing (main)

{-| Plays a list of messages through Counter.update, the way the runtime would
after clicks, and reports the model after each one.
-}

import Counter exposing (Msg(..))


port out : String -> Cmd msg


clicks : List Msg
clicks =
    [ Add, Add, Remove, Reset, Remove ]


label : Msg -> String
label msg =
    case msg of
        Add ->
            "Add"

        Remove ->
            "Remove"

        Reset ->
            "Reset"


step : Msg -> ( List String, Counter.Model ) -> ( List String, Counter.Model )
step msg ( lines, model ) =
    let
        ( next, _ ) =
            Counter.update msg model
    in
    ( lines ++ [ String.padRight 8 ' ' (label msg) ++ "cups = " ++ String.fromInt next.cups ], next )


report : String
report =
    let
        ( start, _ ) =
            Counter.init ()

        ( lines, _ ) =
            List.foldl step ( [ "init    cups = " ++ String.fromInt start.cups ], start ) clicks
    in
    String.join "\n" lines


main : Program () () Never
main =
    Platform.worker
        { init = \_ -> ( (), out report )
        , update = \_ model -> ( model, Cmd.none )
        , subscriptions = \_ -> Sub.none
        }
Build the counter, replay five clicks, batch three commands Python · tea_session.py
"""Builds the Counter element and measures it, replays five clicks through its update function headlessly, and sends
three commands through one port with Cmd.batch (session.py plays it)."""

from session import cache, run

cache()
run("""
cd tea
elm make src/Counter.elm --optimize --output=build/counter.js
wc -c < build/counter.js
elm make src/Replay.elm --optimize --output=build/replay.js
node run.cjs Replay
elm make src/Batch.elm --optimize --output=build/batch.js
node run.cjs Batch
""")

Output

$ cd tea
$ elm make src/Counter.elm --optimize --output=build/counter.js
Dependencies ready!
Success! Compiled 1 module.

    Counter ───> build/counter.js
$ wc -c < build/counter.js
  110515
$ elm make src/Replay.elm --optimize --output=build/replay.js
Success! Compiled 1 module.

    Replay ───> build/replay.js
$ node run.cjs Replay
init    cups = 0
Add     cups = 1
Add     cups = 2
Remove  cups = 1
Reset   cups = 0
Remove  cups = 0
$ elm make src/Batch.elm --optimize --output=build/batch.js
Success! Compiled 1 module.

    Batch ───> build/batch.js
$ node run.cjs Batch
third
second
first

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tea_session.py

The replay printed the count after each message: 1, 2, 1, 0, and 0 again, because Remove at 0 keeps the count at 0 with max 0. The optimised counter.js is 110,515 bytes, against 65,308 for the worker of the first lesson, because it carries the virtual DOM and the browser runtime. Counter is a Browser.element, one of five kinds of program:

Program What it owns What it may use
Browser.sandbox one node of a page init, update, view; no commands, subscriptions, flags or ports
Browser.element one node of a page also Cmd, Sub, flags and ports
Browser.document the page’s <title> and <body> as element
Browser.application the whole page and its URLs as document, plus onUrlRequest and onUrlChange
Platform.worker no page at all init, update, subscriptions, flags and ports

Test update, not the page

Most of a program’s behaviour is in update, and update is pure, so tests can call it with a model and a message and compare what comes back, as Replay does. The project lesson of this module tests its core that way with elm-test.

Checkpoint

Check: The parts of The Elm Architecture

  1. Question 1 of 2 In Counter, who calls update, and what happens to the model it returns?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The runtime calls it with each message and keeps the returned model, then calls view with it

    The runtime runs the loop: message, update, new model, view, page. That is why Replay could call Counter.update itself, five times, without any page.

  2. Question 2 of 2 A widget inside an existing page must receive a user id from JavaScript at start-up and report a total back. Which kind of program fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Browser.element, which owns one node and may use flags, commands, subscriptions and ports

    sandbox for learning, element for one part of a page that talks to JavaScript, document and application for the whole page, and worker for headless logic.

Cmd and Sub are requests that the runtime carries out

update returns a pair: the next model and a command (Cmd): A value of type Cmd Msg that asks the Elm runtime to do something outside the program, such as send an HTTP request or a value through a port. update returns it; the runtime carries it out and may answer with a message., a value of type Cmd Msg that asks the runtime to do something, such as send a request, read the time or send a value out through a port. Cmd.none asks for nothing. A subscription (Sub): A value of type Sub Msg that names outside events a program wants to hear about, such as clock ticks or values sent to a port. The runtime turns each event into a message for update., Sub Msg, is the other direction: subscriptions says which outside events the program wants to hear about, and the runtime turns each one into a message for update. Neither is an action you take: both are descriptions that the runtime carries out, which is why the same update runs in a browser, under Node.js and in a test.

The last program of the session, Batch.elm, hands the runtime three commands for one port in one Cmd.batch: out "first", out "second" and out "third". The subscriber received them as third, second, first.

Batch.elm: three commands in one Cmd.batch ELM · tea/src/Batch.elm
port module Batch exposing (main)

{-| Three commands for one port, handed to the runtime in one Cmd.batch.
-}


port out : String -> Cmd msg


main : Program () () Never
main =
    Platform.worker
        { init = \_ -> ( (), Cmd.batch [ out "first", out "second", out "third" ] )
        , update = \_ model -> ( model, Cmd.none )
        , subscriptions = \_ -> Sub.none
        }

That is not a bug. The documentation of Cmd.batch says that each command is handed to the runtime at the same time and that there are no ordering guarantees about the results. When order matters, send one message that carries everything in order, a list or one string, as every worker of this module does.

Relying on the order of a batch

Sending the lines of a report as Cmd.batch (List.map out lines) hands them to the runtime in no promised order, as Batch showed with three. Join the lines into one string, or send a List String through the port, and JavaScript gets them in order.

Checkpoint

Check: Commands and subscriptions

  1. Question 1 of 2 update returns ( next, totals { count = 1, paise = 1200 } ). When does the value reach JavaScript?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When the runtime carries out the command, after update has returned

    Commands and subscriptions describe effects. update stays pure, and the runtime does the sending, which is also why the till's totals came out after the script's own lines.

  2. Question 2 of 2 Batch sent Cmd.batch [ out "first", out "second", out "third" ]. What did the subscriber print, and why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: third, second, first: a batch hands its commands over together, with no promise about order

    When order matters, send one message that carries the values in order, such as one string or a list, instead of a batch of commands.

Decoders check JSON at the border and name the bad field

JSON from a server is untyped text. Elm turns it into typed values with a JSON decoder: An elm/json value of type Decoder a that describes the shape a JSON value must have and turns it into an Elm value of type a, or into an error that names the path of the first value that did not fit., a value that describes the shape it expects. OrderDecoder.elm reads "version" first and then the fields of that version, every item’s name, price in paise and quantity, and an optional note. The session first moves elm/json from the indirect to the direct dependencies with elm install, offline, because the program imports it now:

OrderDecoder.elm: two versions of an order, a strict note, an encoder ELM · json/src/OrderDecoder.elm
module OrderDecoder exposing (Item, Order, decoder, encode, looseDecoder)

{-| Decoders for an order sent by a server as JSON, in two versions, and an
encoder for sending one back.
-}

import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode


type alias Item =
    { name : String
    , paise : Int
    , quantity : Int
    }


type alias Order =
    { id : String
    , items : List Item
    , note : Maybe String
    }


{-| Reads "version" first, then the fields of that version.
-}
decoder : Decoder Order
decoder =
    Decode.field "version" Decode.int
        |> Decode.andThen forVersion


forVersion : Int -> Decoder Order
forVersion version =
    case version of
        1 ->
            Decode.map3 Order
                (Decode.field "id" Decode.string)
                (Decode.field "lines" (Decode.list itemV1))
                (Decode.succeed Nothing)

        2 ->
            Decode.map3 Order
                (Decode.field "id" Decode.string)
                (Decode.field "items" (Decode.list item))
                (optionalField "note" Decode.string)

        _ ->
            Decode.fail ("version " ++ String.fromInt version ++ " is not one this app reads")


itemV1 : Decoder Item
itemV1 =
    Decode.map3 Item
        (Decode.field "item" Decode.string)
        (Decode.field "paise" Decode.int)
        (Decode.field "qty" Decode.int)


item : Decoder Item
item =
    Decode.map3 Item
        (Decode.field "name" Decode.string)
        (Decode.field "paise" Decode.int)
        (Decode.field "quantity" Decode.int)


{-| A field that may be missing or null; when it is there, it must decode.
-}
optionalField : String -> Decoder a -> Decoder (Maybe a)
optionalField name inner =
    Decode.keyValuePairs Decode.value
        |> Decode.andThen
            (\pairs ->
                if List.any (\( key, _ ) -> key == name) pairs then
                    Decode.field name (Decode.nullable inner)

                else
                    Decode.succeed Nothing
            )


{-| The same version 2 order, read with Decode.maybe for the note: a note of the
wrong type is quietly dropped.
-}
looseDecoder : Decoder Order
looseDecoder =
    Decode.map3 Order
        (Decode.field "id" Decode.string)
        (Decode.field "items" (Decode.list item))
        (Decode.maybe (Decode.field "note" Decode.string))


encode : Order -> Encode.Value
encode order =
    Encode.object
        [ ( "version", Encode.int 2 )
        , ( "id", Encode.string order.id )
        , ( "items"
          , Encode.list
                (\i -> Encode.object [ ( "name", Encode.string i.name ), ( "paise", Encode.int i.paise ), ( "quantity", Encode.int i.quantity ) ])
                order.items
          )
        , ( "note", Maybe.map Encode.string order.note |> Maybe.withDefault Encode.null )
        ]

DecodeDemo.elm decodes each file of samples/ and prints what came out; run.cjs reads the files and passes them as flags, a list of name and text pairs:

samples/order-4-text-price.json: one price sent as text JSON · json/samples/order-4-text-price.json
{"version": 2, "id": "A-19", "items": [{"name": "chai", "paise": 1200, "quantity": 2}, {"name": "samosa", "paise": "1450", "quantity": 3}]}

The same document, added up in JavaScript with no decoder in between, gave a right total by luck and a wrong sum: 1450 * 3 and "1450" * 3 are both 4350, but 1200 + "1450" turns the number into text and joins the two:

sum_in_js.mjs: the text price in a JavaScript total and a sum JavaScript · json/sum_in_js.mjs
// The order of samples/order-4-text-price.json, added up in JavaScript with no decoder in between.
import { readFileSync } from 'node:fs';

const order = JSON.parse(readFileSync(new URL('./samples/order-4-text-price.json', import.meta.url), 'utf8'));

const total = order.items.reduce((sum, item) => sum + item.paise * item.quantity, 0);
const prices = order.items.reduce((sum, item) => sum + item.paise, 0);

console.log(`total of the lines: ${total}`);
console.log(`sum of the prices:  ${prices}`);

Output

total of the lines: 6750
sum of the prices:  12001450

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sum_in_js.mjs

Now the Elm side, with every document of samples/:

elm install elm/json, then six documents decoded Python · json_session.py
"""Moves elm/json from the indirect to the direct dependencies with elm install, offline, then builds DecodeDemo and
decodes the six documents of samples/ with it (session.py plays it). elm install asks before it changes elm.json, and
the session answers y."""

from session import cache, run

cache()
run("""
cd json
elm install elm/json
elm make src/DecodeDemo.elm --optimize --output=build/decode.js
node run.cjs
""", stdin="y\n")

Output

$ cd json
$ elm install elm/json
I found it in your elm.json file, but in the "indirect" dependencies.
Should I move it into "direct" dependencies for more general use? [Y/n]: Success!
$ elm make src/DecodeDemo.elm --optimize --output=build/decode.js
Success! Compiled 2 modules.

    DecodeDemo ───> build/decode.js
$ node run.cjs
order-1-v2.json
  Ok A-17: 2 items, 6750 paise, note "less sugar"
order-2-v1.json
  Ok A-9: 1 item, 1200 paise, no note
order-3-no-note.json
  Ok A-18: 1 item, 1200 paise, no note
order-4-text-price.json
  Err Problem with the value at json.items[1].paise:

      "1450"

  Expecting an INT
order-5-note-number.json
  Err The Json.Decode.oneOf at json.note failed in the following 2 ways:



  (1) Problem with the given value:

      42

      Expecting null



  (2) Problem with the given value:

      42

      Expecting a STRING
order-6-version-3.json
  Err Problem with the given value:

  {
          "version": 3,
          "id": "A-21",
          "items": []
      }

  version 3 is not one this app reads
order-5-note-number.json with Decode.maybe
  Ok A-20: 0 items, 0 paise, no note
encoded again
  {"version":2,"id":"A-17","items":[{"name":"chai","paise":1200,"quantity":2},{"name":"samosa","paise":1450,"quantity":3}],"note":"less sugar"}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 json_session.py

Three documents became orders: version 2 with a note, version 1 converted to the same Order, and one with no note, read as Nothing. The other three were refused, each with a path: json.items[1].paise for the price sent as "1450"; json.note, where 42 failed both ways the note may be read, null or a string; and the version 3 document, which the decoder rejected with its own message through Decode.fail. Follow the documents through the decoder:

Interactive · stage by stage How the order decoder reads one document Step through the decoder on the six samples, and see where each one is accepted or stopped, with its path.
How the order decoder reads one document, step 6 of 6. Showing A JSON document, field "version", Decode.andThen, Each item, The note, Ok Order. In focus: Ok Order.Err: version 3Err: json.items[1].paiseErr: json.noteA JSON documenttext from the serverfield "version"an Int, first of allDecode.andThenversion 1, 2, or failEach itemname, paise, quantityThe notemissing, null or a StringOk OrderA-17: 2 items, 6750 paise

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Version first. The decoder reads one field before anything else: "version", which must be an Int. Every document of the samples has one.
  2. Choose the shape. Decode.andThen picks the rest of the decoder from the value: version 1 has "lines" with "item" and "qty", version 2 has "items" with "name" and "quantity".
  3. An unknown version. For version 3 the decoder fails on purpose, with Decode.fail and its own message, and the Err carries the whole document.
  4. Every item, every field. Each item needs a String name and Int paise and quantity. In order-4, the second item has the price "1450", a string, so the Err names the path json.items[1].paise and says Expecting an INT.
  5. The note, strictly. The note may be missing or null, both read as Nothing. When it is there it must be a String: a note of 42 gives an Err at json.note, while Decode.maybe would have turned it into Nothing without a word.
  6. An Order, or a reason. A document that passes every step becomes an Order, here A-17 with 2 items and 6750 paise. Nothing that failed reaches update: the error is a value you can show or log.

The last two entries of the report matter. Read with Decode.maybe, the same document with "note": 42 was accepted as an order with no note: Decode.maybe turns a value of the wrong type into Nothing as quietly as a missing one, which hides a broken server. optionalField accepts a missing field or null, and refuses anything else. Encoding is the mirror image: Json.Encode builds the value, and Encode.encode 0 turned the first order back into one line of JSON.

Key idea

Decode every JSON value at the border into the types your program uses; a decoding error is a value with the path of the bad field, so nothing malformed reaches update, and Decode.maybe is only for fields whose type may really vary.

JSON Validator Paste a server's response to check that it is valid JSON before you write its decoder.

Checkpoint

Check: JSON decoders and their errors

  1. Question 1 of 2 The second item of order-4 has "paise": "1450". What did the decoder report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Err, with the path json.items[1].paise and Expecting an INT

    A decoding error is a value with the path of the first bad field, so the program can show it or log it, and nothing malformed reaches update.

  2. Question 2 of 2 order-5 has "note": 42. What did Decode.maybe (Decode.field "note" Decode.string) give, and why is that risky?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing, the same as a missing note, so a broken server goes unnoticed

    Decode.maybe cannot tell a missing field from one of the wrong type. A strict optional decoder accepts a missing field or null and refuses anything else.

Flags and ports carry values across the border, and check their type

Elm reaches JavaScript in three ways: flags: The one value JavaScript passes to an Elm program when it starts it, as Elm.Main.init({ flags: … }). init receives it; a value of the wrong type makes init throw unless the flags are declared as Json.Decode.Value and decoded., one value given to init at start-up; ports, for values in both directions while the program runs; and custom elements, JavaScript behind a tag that the view places. Till.elm uses the first two. It starts with the shop’s name as a flag, hears prices through the incoming port prices and sends the running total out through totals:

Till.elm: a flag, an incoming port and an outgoing port ELM · till/src/Till.elm
port module Till exposing (main)

{-| A till without a screen. JavaScript gives the shop's name as a flag, sends
prices in paise through the port prices, and hears the running total through
the port totals.
-}


port prices : (Int -> msg) -> Sub msg


port totals : { count : Int, paise : Int } -> Cmd msg


type alias Flags =
    { shop : String }


type alias Model =
    { shop : String
    , count : Int
    , paise : Int
    }


type Msg
    = Scanned Int


init : Flags -> ( Model, Cmd Msg )
init flags =
    ( { shop = flags.shop, count = 0, paise = 0 }, Cmd.none )


update : Msg -> Model -> ( Model, Cmd Msg )
update (Scanned price) model =
    let
        next =
            { model | count = model.count + 1, paise = model.paise + price }
    in
    ( next, totals { count = next.count, paise = next.paise } )


main : Program Flags Model Msg
main =
    Platform.worker
        { init = init
        , update = update
        , subscriptions = \_ -> prices Scanned
        }

run.cjs sends three prices, then a price as the text '1450', then starts a second till with { shop: 42 }. Both tills are development builds, so that every error carries its full message:

run.cjs: good prices, a price as text, flags of the wrong type JavaScript · till/run.cjs
// The typed till: three prices, then a price sent as text, then a start with a flag of the wrong type.
const { Elm } = require('./build/till.js');

const app = Elm.Till.init({ flags: { shop: 'Corner Cafe' } });
app.ports.totals.subscribe((t) => console.log(`${t.count} item${t.count === 1 ? '' : 's'}, ${t.paise} paise`));
for (const price of [1200, 1450, 1450]) app.ports.prices.send(price);
console.log('sent three prices');
try {
  app.ports.prices.send('1450');
} catch (error) {
  console.log(`send('1450') threw: ${error.message}`);
}
try {
  Elm.Till.init({ flags: { shop: 42 } });
} catch (error) {
  console.log(`init with { shop: 42 } threw: ${error.message}`);
}
Both tills, given the same prices Python · ports_session.py
"""Builds the two tills of till/ in development mode, so that errors carry their full messages, and runs each with the
same prices, one of them sent as text (session.py plays it)."""

from session import cache, run

cache()
run("""
cd till
elm make src/Till.elm --output=build/till.js
node run.cjs
elm make src/TillSafe.elm --output=build/tillsafe.js
node safe.cjs
""")

Output

$ cd till
$ elm make src/Till.elm --output=build/till.js
Dependencies ready!
Success! Compiled 1 module.

    Till ───> build/till.js
$ node run.cjs
Compiled in DEV mode. Follow the advice at https://elm-lang.org/0.19.3/optimize for better performance and smaller assets.
sent three prices
send('1450') threw: Trying to send an unexpected type of value through port `prices`:
[object Object]
init with { shop: 42 } threw: Problem with the flags given to your Elm program on initialization.

Problem with the value at json.shop:

    42

Expecting a STRING
1 item, 1200 paise
2 items, 2650 paise
3 items, 4100 paise
$ elm make src/TillSafe.elm --output=build/tillsafe.js
Success! Compiled 1 module.

    TillSafe ───> build/tillsafe.js
$ node safe.cjs
Compiled in DEV mode. Follow the advice at https://elm-lang.org/0.19.3/optimize for better performance and smaller assets.
1 item, 1200 paise
2 items, 2650 paise
rejected: Problem with the given value: "1450" Expecting an INT
3 items, 4100 paise

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ports_session.py

Three things happened. The totals came out after the line sent three prices and after both errors: send returns at once, and the outgoing messages reach the subscriber later. The text '1450' made send throw in the JavaScript that called it, with a message that ends in [object Object] instead of the reason. And the flags { shop: 42 } made init throw, this time with a clear message: Problem with the value at json.shop, Expecting a STRING. The flags chapter of the guide calls this failing fast, and notes that many programs take their flags as a Json.Decode.Value and decode them in Elm, to decide themselves what a bad value means. The second till does the same for its port: TillSafe turned '1450' into rejected: Problem with the given value: "1450" Expecting an INT and counted the next price as usual:

TillSafe.elm: a port that takes any value and decodes it ELM · till/src/TillSafe.elm
port module TillSafe exposing (main)

{-| The till with a port that takes any JavaScript value and decodes it in Elm,
so a value of the wrong type becomes a message instead of an exception.
-}

import Json.Decode as Decode


port prices : (Decode.Value -> msg) -> Sub msg


port totals : String -> Cmd msg


type alias Model =
    { count : Int
    , paise : Int
    }


type Msg
    = Scanned Decode.Value


update : Msg -> Model -> ( Model, Cmd Msg )
update (Scanned value) model =
    case Decode.decodeValue Decode.int value of
        Ok price ->
            let
                next =
                    { count = model.count + 1, paise = model.paise + price }
            in
            ( next, totals (items next.count ++ ", " ++ String.fromInt next.paise ++ " paise") )

        Err error ->
            ( model, totals ("rejected: " ++ String.join " " (String.words (Decode.errorToString error))) )


items : Int -> String
items count =
    if count == 1 then
        "1 item"

    else
        String.fromInt count ++ " items"


main : Program () Model Msg
main =
    Platform.worker
        { init = \_ -> ( { count = 0, paise = 0 }, Cmd.none )
        , update = update
        , subscriptions = \_ -> prices Scanned
        }
Interactive · message by message Values crossing the border, both ways Step through the till's start, three prices, and a price sent as text, first to a typed port, then to one that decodes.
Values crossing the border, both ways, step 7 of 7. Messages so far: run.cjs to Elm runtime: { shop: 'Corner Cafe' }; run.cjs to Elm runtime: send(1200); Elm runtime to The till: Scanned 1200; Elm runtime to run.cjs: 1 item, 1200 paise; run.cjs to Elm runtime: send('1450'); Elm runtime to The till: Scanned value.run.cjsNode.jsElm runtimechecks each valueThe tillinit and update{ shop: 'Corner Cafe' }send(1200)Scanned 12001 item, 1200 paisesend('1450')Scanned value

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Flags at the start. Elm.Till.init({ flags: { shop: 'Corner Cafe' } }) gives init one value. The runtime checks it against the Flags type before init runs.
  2. A price comes in. app.ports.prices.send(1200) hands the runtime a JavaScript number. prices is declared (Int -> msg) -> Sub msg, so the number must be an Int, and it is.
  3. update runs. The runtime wraps the value in the message Scanned 1200 and calls update, which adds it to the model and returns a totals command.
  4. Totals go out later. The totals for all three prices reached the subscriber only after run.cjs had printed sent three prices and both error lines: send returns before the outgoing messages are delivered.
  5. Text where an Int belongs. send('1450') gives a String to a port that wants an Int. The runtime refuses it by throwing in the JavaScript that called send, and the development build's message ends with [object Object].
  6. Flags of the wrong type. A second start with { shop: 42 } fails the same way, but with a full message: Problem with the value at json.shop, Expecting a STRING.
  7. Decode it in Elm. TillSafe declares its port with Json.Decode.Value, so any value gets in, and update decodes it. '1450' becomes rejected: … Expecting an INT, and the next price still counts.

Checkpoint

Check: Flags and ports

  1. Question 1 of 2 prices is declared (Int -> msg) -> Sub msg. What happened when run.cjs called app.ports.prices.send('1450')?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: send threw an error in run.cjs, before any Elm code saw the value

    Typed flags and ports fail fast, on the JavaScript side. Declaring them as Json.Decode.Value moves the check into Elm, where a bad value becomes a message.

  2. Question 2 of 2 Why did TillSafe keep counting after it was sent '1450'?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Its port takes a Json.Decode.Value and update decodes it, so a bad value becomes a rejected message instead of an exception

    Ports and flags of type Json.Decode.Value let Elm check what JavaScript sends with a decoder, so the program decides what a bad value means.

Html.text keeps text as text, and so must the JavaScript side

A name typed by a user, such as Asha <b>VIP</b>, must reach the page as those characters, not as a bold tag: markup that comes from input is how cross-site scripting (XSS): A weakness in which text from a user or another site reaches a page as markup or script instead of as text, so the browser runs it with the page's rights. Writing such text with textContent instead of innerHTML prevents it. starts. Inside Elm the virtual DOM guards it. elm/virtual-dom 1.0.5 draws Html.text with createTextNode, which never reads markup; it turns a script element made with Html.node into a p; it renames attributes that start with on, and the innerHTML and outerHTML properties, to harmless data- names; and in an optimised build it replaces a javascript: link with an empty string. A view cannot ask for markup from a string.

The JavaScript side of a port has no such guard. A subscriber that writes a message with el.innerHTML = … hands the browser markup to parse, and OWASP’s cross-site scripting cheat sheet names innerHTML as an unsafe sink and textContent as the safe one: it creates a text node without parsing HTML. text_check.mjs shows the difference in Node.js, which has no page: a weak function joins the message into HTML as innerHTML would receive it, a fixed one escapes it as a text node keeps it, and a check counts the elements in each result:

text_check.mjs: the same message, joined and escaped, and a check JavaScript · text_check.mjs
// Two ways a port subscriber could put a message from a user on the page, and a check that tells them apart.
const message = 'Asha <b>VIP</b>';

// Weak: joins strings into HTML, as el.innerHTML = '<p>' + message + '</p>' would.
const weak = (text) => '<p>' + text + '</p>';

// Fixed: escapes the characters HTML reads as markup, so the text stays text, as el.textContent = message does.
const ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => ESCAPES[c]);
const fixed = (text) => '<p>' + escapeHtml(text) + '</p>';

// The check: the only element in the result is the <p> the code itself wrote.
const elements = (html) => (html.match(/<[a-z]/gi) ?? []).length;

for (const [name, render] of [['weak', weak], ['fixed', fixed]]) {
  const html = render(message);
  const verdict = elements(html) === 1 ? 'PASS: one element, the message is text' : `FAIL: ${elements(html)} elements, the message added markup`;
  console.log(`${name.padEnd(5)} ${html}`);
  console.log(`      ${verdict}`);
}

Output

weak  <p>Asha <b>VIP</b></p>
      FAIL: 2 elements, the message added markup
fixed <p>Asha &lt;b&gt;VIP&lt;/b&gt;</p>
      PASS: one element, the message is text

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node text_check.mjs

Where the text goes What happens to Asha <b>VIP</b>
Html.text name in an Elm view a text node: the page shows the characters
el.textContent = name in a port subscriber a text node: the page shows the characters
el.innerHTML = '<p>' + name + '</p>' parsed as markup: a second element, the check’s FAIL

Text in, text out, on both sides

Show values from ports with textContent, set attributes with setAttribute on a fixed name that is never an event handler such as onclick, and never build HTML strings from them; in a browser pass the raw text to textContent, without escaping it first. The weak line of text_check.mjs failed its check and the fixed line passed: keep a check like it next to every subscriber that writes to the page.

Checkpoint

Check: Keeping text from users as text

  1. Question 1 of 2 An Elm view shows a name with Html.text name, and the name is Asha <b>VIP</b>. What does the page show?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The characters Asha <b>VIP</b>, because Html.text makes a text node

    Inside Elm the virtual DOM keeps text as text. The risk is on the JavaScript side, where a subscriber may write the same text with innerHTML.

  2. Question 2 of 2 A port subscriber shows a message from Elm in a toast. Which line keeps a message such as Asha <b>VIP</b> from adding markup?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: toast.textContent = message

    Write values from ports with textContent, or with setAttribute on a fixed attribute name that is not an event handler. Never build HTML from them.

Practise: decode an order, then update a cart

A lab of two JavaScript exercises, whose sample tests the page runs for you. In the first you write the order decoder’s rules by hand, an error path for every bad value included, which shows how much a decoder checks; its tests are the six documents of samples/ and a few more. In the second you write a cart’s update as the runtime would call it, one message and one model in and the next model out, against tests that freeze every model, so a change made in place fails at once:

Code lab · 2 steps

Decode at the border, then update a model

Read an order from JSON with an error path for the first bad value, then write a cart's update that never changes the model it gets.

  1. Decode an order with a path in every error To do
  2. Write a cart's update the way Elm runs it To do

Exercise · Medium · JavaScript

Decode an order with a path in every error

Write decodeOrder(text) in decode.mjs: it reads an order sent as JSON text with the rules of OrderDecoder.elm and returns { ok: true, order } or { ok: false, error }, where error names the path of the first bad value the way elm/json does, such as "json.items[1].paise: expecting an Int".

  • "version" comes first and must be a whole number (json.version: expecting an Int). Version 1 has "lines" whose items have "item", "paise" and "qty"; version 2 has "items" whose items have "name", "paise" and "quantity". Any other version gives json.version: version 3 is not one this app reads (with its number).
  • "id" must be a string (json.id: expecting a String), and every item's name a string and its paise and quantity whole numbers, checked in that order, item by item, with the item's index in the path.
  • In version 2, "note" may be missing or null, both read as null; if it is anything else it must be a string (json.note: expecting a String or null). Version 1 orders have no note.
  • Text that is not JSON gives json: not valid JSON.

order is { id, items: [{ name, paise, quantity }], note } for both versions. The sample tests use the six documents of the lesson's samples/ folder and a few more edge cases.

Starter code · decode.mjs

/** An order from JSON text, as OrderDecoder.elm reads it: { ok: true, order } or { ok: false, error }. */
export function decodeOrder(text) {
  // Replace this: it trusts the server, so a price sent as "1450" goes through as text and a missing field as undefined.
  const json = JSON.parse(text);
  const items = (json.items ?? json.lines ?? []).map((i) => ({ name: i.name ?? i.item, paise: i.paise, quantity: i.quantity ?? i.qty }));
  return { ok: true, order: { id: json.id, items, note: json.note ?? null } };
}
The sample tests · decode.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { decodeOrder } from './decode.mjs';

// The six documents of the lesson's samples/ folder.
const order1 = '{"version": 2, "id": "A-17", "items": [{"name": "chai", "paise": 1200, "quantity": 2}, {"name": "samosa", "paise": 1450, "quantity": 3}], "note": "less sugar"}';
const order2 = '{"version": 1, "id": "A-9", "lines": [{"item": "chai", "paise": 1200, "qty": 1}]}';
const order3 = '{"version": 2, "id": "A-18", "items": [{"name": "chai", "paise": 1200, "quantity": 1}]}';
const order4 = '{"version": 2, "id": "A-19", "items": [{"name": "chai", "paise": 1200, "quantity": 2}, {"name": "samosa", "paise": "1450", "quantity": 3}]}';
const order5 = '{"version": 2, "id": "A-20", "items": [], "note": 42}';
const order6 = '{"version": 3, "id": "A-21", "items": []}';

test('version 2 with a note, and version 1, become the same shape', () => {
  assert.deepEqual(decodeOrder(order1), {
    ok: true,
    order: {
      id: 'A-17',
      items: [
        { name: 'chai', paise: 1200, quantity: 2 },
        { name: 'samosa', paise: 1450, quantity: 3 },
      ],
      note: 'less sugar',
    },
  });
  assert.deepEqual(decodeOrder(order2), { ok: true, order: { id: 'A-9', items: [{ name: 'chai', paise: 1200, quantity: 1 }], note: null } });
});

test('a missing note or a null note is null', () => {
  assert.deepEqual(decodeOrder(order3), { ok: true, order: { id: 'A-18', items: [{ name: 'chai', paise: 1200, quantity: 1 }], note: null } });
  assert.deepEqual(decodeOrder('{"version": 2, "id": "A-22", "items": [], "note": null}'), { ok: true, order: { id: 'A-22', items: [], note: null } });
});

test('the first bad value is named by its path', () => {
  assert.deepEqual(decodeOrder(order4), { ok: false, error: 'json.items[1].paise: expecting an Int' });
  assert.deepEqual(decodeOrder(order5), { ok: false, error: 'json.note: expecting a String or null' });
  assert.deepEqual(decodeOrder(order6), { ok: false, error: 'json.version: version 3 is not one this app reads' });
});

test('versions, ids, fractions and fields in order', () => {
  assert.deepEqual(decodeOrder('{"version": "2", "id": "A-1", "items": []}'), { ok: false, error: 'json.version: expecting an Int' });
  assert.deepEqual(decodeOrder('{"version": 2, "items": []}'), { ok: false, error: 'json.id: expecting a String' });
  assert.deepEqual(decodeOrder('{"version": 2, "id": "A-2", "items": [{"name": "chai", "paise": 1200.5, "quantity": 1}]}'), { ok: false, error: 'json.items[0].paise: expecting an Int' });
  assert.deepEqual(decodeOrder('{"version": 1, "id": "A-3", "lines": [{"item": "chai", "paise": 1200, "qty": 1}, {"item": 7, "paise": 1, "qty": "x"}]}'), { ok: false, error: 'json.lines[1].item: expecting a String' });
  assert.deepEqual(decodeOrder('{"version": 2, "id": "A-4", "items": {"name": "chai"}}'), { ok: false, error: 'json.items: expecting a List' });
});

test('text that is not JSON, and JSON that is not an object', () => {
  assert.deepEqual(decodeOrder('{"version": 2,'), { ok: false, error: 'json: not valid JSON' });
  assert.deepEqual(decodeOrder('null'), { ok: false, error: 'json.version: expecting an Int' });
  assert.deepEqual(decodeOrder('[2]'), { ok: false, error: 'json.version: expecting an Int' });
});
A hint

Write one small checker per kind of value, each taking the value and its path, such as int(value, 'json.items[1].paise'), and throwing an error with that message when the value is wrong. decodeOrder then calls them in field order inside one try, and turns the first error into { ok: false, error }. Number.isInteger tells a whole number from 1450.5 and from the string "1450".

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · JavaScript

Write a cart's update the way Elm runs it

In The Elm Architecture, update takes a message and the current model and returns the next model; the runtime keeps it, and nothing else changes. Write the update of a small cart in cart.mjs with the same rules, in JavaScript, so that it could stand in for Counter.update with more messages:

  • The model is { lines: [{ item, quantity }] }, the lines in the order their items were first added.
  • { tag: 'Add', item } adds one of item: its line's quantity grows by 1, or a new line { item, quantity: 1 } goes at the end.
  • { tag: 'Remove', item } takes one away; a line that reaches 0 disappears; removing an item that is not in the cart returns a model equal to the one given.
  • { tag: 'Clear' } empties the cart.
  • Any other tag throws an Error that names it, because in Elm such a message could not even be written.

update must never change the model it receives: the tests freeze it, so assigning to it fails. The starter code changes the model in place, the habit that The Elm Architecture rules out.

Starter code · cart.mjs

/** The next cart for one message: Add, Remove or Clear. It never changes `model`. */
export function update(msg, model) {
  // Replace this: it changes the model it was given, and it forgets the Remove and Clear rules.
  if (msg.tag === 'Add') {
    const line = model.lines.find((l) => l.item === msg.item);
    if (line) line.quantity += 1;
    else model.lines.push({ item: msg.item, quantity: 1 });
  }
  return model;
}
The sample tests · cart.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { update } from './cart.mjs';

/** A deep-frozen copy: in a module, assigning to it or pushing onto it throws a TypeError. */
function frozen(value) {
  if (typeof value === 'object' && value !== null) {
    for (const v of Object.values(value)) frozen(v);
    Object.freeze(value);
  }
  return value;
}

const empty = () => frozen({ lines: [] });

/** Runs messages through update, as the runtime would, freezing each model on the way. */
function play(messages, start = empty()) {
  return messages.reduce((model, msg) => frozen(update(msg, model)), start);
}

test('Add makes a line, then counts up', () => {
  assert.deepEqual(play([{ tag: 'Add', item: 'chai' }]), { lines: [{ item: 'chai', quantity: 1 }] });
  assert.deepEqual(play([{ tag: 'Add', item: 'chai' }, { tag: 'Add', item: 'samosa' }, { tag: 'Add', item: 'chai' }]), {
    lines: [
      { item: 'chai', quantity: 2 },
      { item: 'samosa', quantity: 1 },
    ],
  });
});

test('Remove counts down, and a line at 0 disappears', () => {
  const two = play([{ tag: 'Add', item: 'chai' }, { tag: 'Add', item: 'chai' }, { tag: 'Add', item: 'samosa' }]);
  assert.deepEqual(play([{ tag: 'Remove', item: 'chai' }], two), { lines: [{ item: 'chai', quantity: 1 }, { item: 'samosa', quantity: 1 }] });
  assert.deepEqual(play([{ tag: 'Remove', item: 'samosa' }], two), { lines: [{ item: 'chai', quantity: 2 }] });
  assert.deepEqual(play([{ tag: 'Remove', item: 'vada' }], two), two);
});

test('Clear empties the cart', () => {
  assert.deepEqual(play([{ tag: 'Add', item: 'chai' }, { tag: 'Clear' }]), { lines: [] });
});

test('the model given is never changed', () => {
  const before = play([{ tag: 'Add', item: 'chai' }]);
  const after = update({ tag: 'Add', item: 'chai' }, before);
  assert.deepEqual(before, { lines: [{ item: 'chai', quantity: 1 }] });
  assert.deepEqual(after, { lines: [{ item: 'chai', quantity: 2 }] });
});

test('an unknown message is refused', () => {
  assert.throws(() => update({ tag: 'Checkout' }, empty()), /Checkout/);
});
A hint

Build the next model instead of editing this one: model.lines.map(...) to change one line into a new object { ...line, quantity: line.quantity + 1 }, filter to drop a line at 0, and [...model.lines, { item, quantity: 1 }] to add one. A switch on msg.tag with a default that throws covers the last rule.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Last, a self-check across the architecture, commands, decoders, ports and safe text. Your shakiest of those five comes up first, and each choice you make is answered with the reason it is right or what it misses:

Practice that adapts to you

Practise this lesson

12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What does the Msg type of Counter, Add | Remove | Reset, describe?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: Everything that can happen to the counter, one variant for each

    The model is what the program knows; the messages are what can happen. update connects them, one case per message.

  2. Question 2 of 12 Counter starts at 0. Replay sends Add, Add, Remove, Reset, Remove. How many cups are there at the end?

    Type a number.

    Show the answer: Question 2 of 12

    Answer: 0 cups

    Add, Add gives 2, Remove gives 1, Reset gives 0, and Remove at 0 stays 0 because of max 0. The session printed the same five counts: 1, 2, 1, 0, 0.

  3. Question 3 of 12 Which of these describes a subscription?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: A value that names outside events, such as values sent to a port, which the runtime turns into messages

    Commands go out and happen once; subscriptions listen for events and keep doing so while subscriptions returns them.

  4. Question 4 of 12 A teammate wants update to call fetch directly "to save a step". Why does Elm return a Cmd instead?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Returning a description keeps update pure, so the same update runs in a browser, in a test and in Replay

    Effects as values keep the core of a program a set of pure functions, which is what makes it testable and lets the runtime decide when and how effects happen.

  5. Question 5 of 12 What does Decode.field "version" Decode.int |> Decode.andThen forVersion let the decoder do?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: Read the version first, then choose the decoder for the rest of the document from it

    Decode.andThen is how a decoder depends on part of its own input, such as a version or a type tag.

  6. Question 6 of 12 sum_in_js.mjs adds up order-4, whose second price is the text "1450", with no decoder in between. What does it print?

    What does this program print? Choose one answer.

    // The order of samples/order-4-text-price.json, added up in JavaScript with no decoder in between.
    import { readFileSync } from 'node:fs';
    
    const order = JSON.parse(readFileSync(new URL('./samples/order-4-text-price.json', import.meta.url), 'utf8'));
    
    const total = order.items.reduce((sum, item) => sum + item.paise * item.quantity, 0);
    const prices = order.items.reduce((sum, item) => sum + item.paise, 0);
    
    console.log(`total of the lines: ${total}`);
    console.log(`sum of the prices:  ${prices}`);
    Show the answer: Question 6 of 12

    Answer: it prints

    total of the lines: 6750
    sum of the prices:  12001450

    JavaScript converts "1450" to a number for *, so the line total comes out right by luck, while + with a string joins text: 1200 + "1450" is "12001450". The Elm decoder refused the same document at json.items[1].paise before any sum could run.

  7. Question 7 of 12 A server will add "discount" to every item in its next version, and old documents will not have it. What is the safest change to the decoder?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Add a version 3 branch that requires discount, and keep versions 1 and 2 as they are

    Strict decoders per version keep the border honest: every document either becomes a complete Order or is refused with a path that says what changed.

  8. Question 8 of 12 Elm.Till.init({ flags: { shop: 42 } }) is called. Till's init expects { shop : String }. What happens?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: init throws in JavaScript: Problem with the value at json.shop, Expecting a STRING

    Typed flags fail fast on the JavaScript side. Flags declared as Json.Decode.Value move the check into a decoder that can choose a fallback.

  9. Question 9 of 12 run.cjs printed "sent three prices" before any total. What does that say about app.ports.prices.send?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: send returns at once, and the outgoing totals reach their subscriber later

    Do not read results right after send in the same function. Subscribe to the outgoing port and react when its message arrives.

  10. Question 10 of 12 A date-picker library sends dates to Elm as strings, objects or null depending on its version. How should the incoming port be declared?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: port pickedDate : (Json.Decode.Value -> msg) -> Sub msg, with a decoder in update that handles each shape

    When JavaScript is not under your control, take a Json.Decode.Value and decode it: every shape becomes a message, and none becomes an exception.

  11. Question 11 of 12 Which of these does elm/virtual-dom 1.0.5 do to keep markup out of an Elm view?

    Choose every answer that is right.

    Show the answer: Question 11 of 12

    Answer:

    • It renames attributes that start with on, and the innerHTML property, to data- names
    • It turns a script element made with Html.node into a p
    • It draws Html.text with createTextNode

    The virtual DOM's guards mean a view cannot ask for markup from a string. Text from users stays text until JavaScript outside Elm writes it somewhere.

  12. Question 12 of 12 text_check.mjs counted the elements in the weak and the fixed result. Why did the weak one fail?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Joining the message into HTML turned its <b> into a second element

    A check like this fails on the weak code and passes on the fix, so it can guard every subscriber that writes to the page.

Interview questions

Warm-up (fresher to mid-level): What are the parts of The Elm Architecture, and who calls them?

A program has a model, the whole state; a message type listing everything that can happen; update, which takes a message and the model and returns the next model with a command; view, which turns a model into a description of the page; and subscriptions, which name the outside events the program listens to. Your code calls none of them: the runtime does. It calls init once, then view with each new model, and update whenever a message arrives from a click, a subscription or the result of a command, and it keeps the model itself. Commands and subscriptions are values that describe what should happen, and the runtime carries them out. That is why update is pure and can be tested alone, as the lesson’s Replay did with five messages.

Two longer questions, one about a port that crashes and one about a server that changes its JSON:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to staff

    Your Elm app shows a payment status that a third-party JavaScript library reports through a port. In production some sessions stop updating, and the console shows "Trying to send an unexpected type of value through port". What is going on, and how do you make that border robust?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    The orders API is moving to a new response format with prices in a nested object, and for a few weeks the server will send both formats. Your Elm app decodes orders. How do you change the decoders, test them and roll out without errors for either format?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are the four pieces of The Elm Architecture?

    The model, holding the whole state; a Msg type listing everything that can happen; update, giving the next model for a message; and view, describing the page for a model.

  2. Who calls update and view in an Elm program?

    The runtime. It calls init once, view for each new model and update for each message, and it keeps the model: your code only returns values, so update can be tested on its own.

  3. How do Browser.sandbox and Browser.element differ?

    Both own one node of a page. sandbox has no commands, subscriptions, flags or ports; element adds Cmd, Sub, flags and ports.

  4. In what order do the results of a Cmd.batch arrive?

    In no promised order: all the commands go to the runtime at once, and Batch's first, second and third came back as third, second, first. Send one message that holds everything in order.

  5. What did the Elm decoder report for an order whose second price was sent as "1450"?

    A failure at json.items[1].paise, Expecting an INT, naming the bad field. JavaScript instead turned the sum 1200 + "1450" into the text 12001450.

  6. Why is Decode.maybe risky for an optional field?

    It reads a value of the wrong type as Nothing, as quietly as a missing one, so "note": 42 passed with no note. A decoder that allows only a missing field or null refuses it.

  7. What happens when JavaScript sends the text '1450' into an Elm port typed Int?

    send throws in the JavaScript that called it. A port of type Json.Decode.Value lets Elm decode the value and turn a bad one into a message, as TillSafe did.

  8. Why must a port subscriber show user text with textContent and never innerHTML?

    innerHTML parses the text as markup, so Asha <b>VIP</b> becomes a bold element, the start of cross-site scripting. textContent makes a text node, as Elm's own Html.text does.

Key takeaways

  • A program is a model, messages, update and view; the runtime calls them and keeps the model, so update is a pure function that a test can call, as Replay did with five clicks.
  • Commands and subscriptions are values the runtime carries out; Cmd.batch promises no order, and the three commands of Batch arrived as third, second, first.
  • A decoder checks JSON at the border and names the path of the first bad value, such as json.items[1].paise; Decode.maybe hides a value of the wrong type, so optional fields need a stricter decoder.
  • Flags and typed ports throw in JavaScript when a value has the wrong type; ports and flags of type Json.Decode.Value let Elm decode the value and handle a bad one as a message.
  • Elm’s virtual DOM keeps text as text, but a port subscriber must use textContent, never innerHTML, for values that came from users.

Terms in this lesson

command (Cmd)
A value of type Cmd Msg that asks the Elm runtime to do something outside the program, such as send an HTTP request or a value through a port. update returns it; the runtime carries it out and may answer with a message.
cross-site scripting (XSS)
A weakness in which text from a user or another site reaches a page as markup or script instead of as text, so the browser runs it with the page's rights. Writing such text with textContent instead of innerHTML prevents it.
flags
The one value JavaScript passes to an Elm program when it starts it, as Elm.Main.init({ flags: … }). init receives it; a value of the wrong type makes init throw unless the flags are declared as Json.Decode.Value and decoded.
JSON decoder
An elm/json value of type Decoder a that describes the shape a JSON value must have and turns it into an Elm value of type a, or into an error that names the path of the first value that did not fit.
subscription (Sub)
A value of type Sub Msg that names outside events a program wants to hear about, such as clock ticks or values sent to a port. The runtime turns each event into a message for update.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.