s/answer-error as a separate effect; :on-error-<key> mirrors :on-<key>A child component has historically had one channel for talking to its
parent: (s/answer value). Cancellation and "the user said no" fit
that channel cleanly — s/cancel and false are legitimate values,
the parent destructures, life goes on.
Real failures don't fit. Three concrete shapes that surfaced while
porting kasten (see #21
and :kasten/edit-form's save flow):
s/await can't sit inside a
cloroutine try) and now needs to inform the parent without
losing the exception's information.The pre-existing workarounds were all in user-space:
;; Option A: sentinel value
(catch Exception _ [(s/answer ::error)])
;; Option B: tagged tuple
(catch Exception ex [(s/answer [:error ex])])
Both leak the discrimination logic into every parent. With B the
parent's :on-saved becomes:
:on-saved (fn [self v]
(if (and (vector? v) (= :error (first v)))
(assoc self :banner (ex-message (second v)))
(assoc self :edit-open? false)))
That's the kind of branching the :on-<key> lookup was designed to
avoid — and it grows unbounded as the parent learns more failure
shapes.
S-5 / errors/build-fragment already handles intra-component
throws (a :render or :handle that throws turns into an in-place
banner). What's missing is the inter-component (child→parent)
symmetric path.
A new effect (s/answer-error ex) (wire form [:answer-error ex])
and a mirror resume convention :on-error-<key> on the parent.
Lookup is three-tier, in order:
:on-error-<key> (where <key> is the success
resume key). Receives the raw exception. :on-<key> does not
fire.:on-<key>. Falls back to it with the
wrapped value [:error ex]. Logs a one-time deprecation warning
per (parent-cdef, resume-key) pair so existing apps can adopt
incrementally without flooding stderr.errors/build-fragment path as a render/handle throw),
SSE stays open.The <key> is derived at fail time, not stored separately. A
child whose :instance/resume is :on-saved causes the kernel to
look for :on-error-saved on the parent's cdef. No new
:instance/resume-error field is needed for the common case; if
explicit independence is wanted later, it can be added without a
wire change (extra kwargs on [:call …]).
defcomponent time — adding a new error
source means editing every parent that listens for that resume.try/catch around s/await so the parent's "handler" can
itself catch the child's throw. Blocked by cloroutine's
inability to traverse try boundaries — see todo.md §3. Even
if that were lifted, it would couple the parent to the child's
control flow, the opposite of what :answer aims for.[:answer-error {:tag :validation :fields …}]). Rejected: the
framework stays untyped. ex-info already gives you a value
carrier; the taxonomy belongs in the app.answer-error arrives after a partial
commit. Out of scope — recovery is the app's responsibility.:on-error-foo mirroring :on-foo. This
generalises cleanly to nested namespaces (:on/foo →
:on-error/foo) and to non-:on- prefixed keys
(:done → :on-error-done — the prefix is unconditional so the
derivation is predictable).(s/answer-error (ex-info …)) is just another effect; the conversation's
observable state changes are deterministic, even though the
exception object isn't EDN-round-trippable. Apps that need their
errors to be EDN-clean can (ex-info msg data) with serialisable
data.(s/answer-error …) inside a defflow
body is supported the same way (s/answer …) is — it pops the
flow frame and routes to the parent. Catching it back in the
flow body still hits the cloroutine try limitation; that's a
pre-existing constraint, not new debt.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |