flan/conditions.org
Joseph Ferano 042b2ce4d0 A restart is not a transaction, and nothing rolls back
If a frame mutates a global and then signals, taking a retry re-runs the
mutation. Control resumes at the restart-case and runs forward; nothing is
undone. Common Lisp has the same property and offers no help either, so this
is written down rather than fixed.

The discipline is that the author chooses where the retry boundary is: a
restart-case above the mutations re-runs them, one below re-runs only what
follows. Put the restart before anything mutates, make the retried section
idempotent, or snapshot what will be re-applied.

It matters more here than in most Lisps because the intended use is a game
loop, and a bad index signalling BoundsError rather than ending the process
made abandoning and retrying a frame an ordinary thing to do.

conditions.org and web/index.html already carried the mechanical half as a
one-line gotcha; those are rewritten in place rather than gaining a second
bullet beside them. spec-conditions.md takes it in section 5, which already
enumerates what a transfer does and does not do. No numbered case changed
meaning.
2026-09-13 09:14:46 +07:00

5.3 KiB

Conditions and restarts — cheatsheet

Why it is shaped this way: spec-conditions.md. Something to poke at: conditions-play.flanflan dev conditions-play.flan, then C-c C-c.

Works

(signal c)                  ; (). Handler returns -> carry on. No handler -> no-op.
(error  c)                  ; Never. Only a transfer gets past; else the program stops.

(handler-bind [(Type [c] body ...) ...] body ...)     ; match by type, no hierarchy

(restart-case BODY          ; BODY and every clause have the same type = the form's
  (name [p T ...] CLAUSE) ...)

(invoke-restart 'name arg ...)  ; Never. Innermost frame offering the name wins.
(defn supplied [n i32] i32
  (restart-case (middle n)
    (use-value [v i32] (* v 2))         ; the answer comes from outside
    (retry     []      7)))

(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
  (supplied 7))                         ; 42

A clause's parameters are slots of the function that wrote it, and the invoker fills a buffer that function owns — by the time a clause runs, the invoking frame has gone. What a clause takes is compared with what was given at run time, count then spelling, because a restart is found by name on a dynamic stack and neither end can see the other.

(defn fetch [n i32] i32
  (restart-case (middle n)              ; its value if nothing transfers
    (use-placeholder [] -1)
    (retry           [] 7)))

(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
  (fetch 2))                            ; -1

defer between the invoke and the target runs, innermost first, before the clause body. errdefer does not.

The break loop

An unhandled error in a dev build stops on the frame that erred, with nothing unwound, and waits. C-c C-b in Emacs lists what is on offer and resumes into the choice; flan:stopped(Missing) in the modeline says it happened.

The list is numbered, and the number is what is chosen. Two frames offering retry both appear and §4's by-name walk can only ever reach the first, so a name cannot say which one is meant — restart-at can.

A restart below the evaluation a break is inside is listed, marked, and refused: C-x C-e runs its thunk through a C frame that holds its own transfer channel, so an unwind aimed past it would stop at the thunk. Choose one offered above it, or abort.

Not yet

handler-case · find-restart · compute-restarts · a clause's report string. Each refused by name with its reason.

A restart with parameters cannot be taken from the break loop: it aims at a frame by position and has nothing to fill the parameters in with, so the clause stops the program rather than running on values no one supplied. Choose one that takes none, or abort.

find-restart and compute-restarts are blocked on a type rather than on effort: §4 gives them (Option Restart) and a list, and there is no Restart type and no list to return one in. The break loop reads the same stack through the agent's socket instead.

Gotchas

  • A handler closes over nothing. It is lifted into its own function. Accumulate into a global, or put the value on the condition.
  • A restart is not a transaction. Control resumes at the restart-case and runs forward from there, so a retry repeats every side effect between it and the target. Nothing rolls back: a global the frame already set stays set, and gets set again. Common Lisp has exactly this property and offers no help either — restarts are not transactional there and are not here. So you choose where the retry boundary is. A restart-case at the top of a frame re-runs everything, mutations included; one placed after the mutations re-runs only what follows. Put the restart before anything mutates, make the retried section idempotent, or snapshot what will be re-applied. This bites harder here than in most Lisps because the point is a game loop — skip the frame, carry on, don't die. Since a bad index signals BoundsError rather than ending the process, abandoning a frame and retrying it is a real thing to do, and a non-idempotent mutation is what makes it go wrong.
  • An unknown restart name is a hard stop. No find-restart to test with.
  • So are the wrong arguments, and for the same reason: nothing static can know what a name will find. The message names both signatures.
  • Lookup is by name; the signature is checked after it. Nothing searches for a frame the arguments would fit. An inner (use-value [s string] ...) shadows an outer (use-value [v i32] ...), so (invoke-restart 'use-value 21) stops the program even though the outer clause would have taken it.
  • No supertype, so nothing can say "any condition".
  • signal cannot hand a value back. Deliberate (§1).
  • A condition must be a struct. return is refused inside either form.

Stops the program, exit 134

unhandled AssetMissing
file.flan:3:25: no restart named nope is active
file.flan:4:7: a defer invoked a restart, which a defer may not do — ...
file.flan:9:12: restart use-value takes (i32), given (string)
file.flan:6:5: restart use-value takes (i32), and whatever took it supplied
               no arguments — ...