Skip to content

Troubleshooting

Two things go wrong, and they arrive differently. A symptom is something you saw: a view that will not update, a caret that jumps, a fallback that never clears. A complaint is something Fresco said: a thrown ex-info carrying a stable :rf.error/… id.

Start from whichever one you have.

Start from a symptom

Every chapter ends with a troubleshooting table for the surface it teaches, and that is where a symptom is worth looking first, because the table sits beside the mechanism that explains it. Go to the chapter that owns the surface you were working on:

Working on Table
Getting a project to build or boot Installation, Getting started
Views, reads, and what re-renders Views and reads, Lists and collections
Event vectors and callbacks Events as data
Text fields, carets, and IME Controlled inputs, Forms
URLs and navigation Routing and navigation
Fetching, mutations, and races Async resources
A foreign React library Interop
A React island Islands
Local UI state Ephemeral state
Animation and enter/exit Motion and presence
Modals, popovers, and focus Overlays and focus
Theme and locale Theming and internationalisation
Tests Testing
Xray and evidence Diagnostics
Error boundaries Errors
Server rendering and hydration SSR and hydration
Something being too slow Performance
A Reagent codebase Migrating from Reagent
Lazy loading and chunks Code splitting and lazy loading
Keyboard, screen readers, and roles Accessibility

Start from a complaint

A Fresco complaint is a thrown ex-info. Its message is the reason with the id in brackets, and the id is in ex-data:

(try
  (render-the-thing)
  (catch :default e
    (let [{:rf.error/keys [id] :keys [where reason]} (ex-data e)]
      (js/console.error id where reason))))

Four slots ride every complaint, and they answer four different questions:

Slot Question it answers
:rf.error/id Which refusal is this? Branch on this one and nothing else
:where Which function refused
:reason Why, in a sentence, for a human
:recovery :no-recovery — the complaint threw; the fix is in :reason

Two more, :view and :source, name the boundary that was rendering and the file and line its defview was written at. They are context, not contract: they are present in a development build inside a declaration or a render extent, and absent — not nil, absent — outside one and in a release build. Read them to help yourself; never branch on them and never require them in a test that must also pass against a production build.

Assert the id, never the message. Messages improve between releases; an id is frozen for the life of the refusal, never reused after it is retired, and is what an error monitor's grouping rule and your own tests should key on.

The complaint index

Every complaint the shipped package raises today, grouped by the surface that raises it. The normative meaning and payload of each id is spec/009-Instrumentation.md, and this page is the reader's route into it.

An id you cannot find here is either not Fresco's or not from this version. Check the namespace first — core, routing and the resources model raise their own — and then check that your application and test-kit versions match. A few further spellings are claimed without being raised — reserved for surfaces not built yet, or dead forever — and they are listed under Ids that are claimed but not raised at the foot of this page.

Hiccup, heads and children

The interpreter refuses a value it would otherwise have to guess at. Where it can repair what you wrote it repairs it silently and there is no complaint here; these are the cases where repairing would mean overruling you.

Taught in Views and reads.

:rf.error/fresco-empty-vector

You wrote [] where hiccup was expected.

A hiccup vector must have a head.

:rf.error/fresco-bad-head

You put something outside the closed head set in hiccup head position.

Named in Views and reads, Lists and collections, Diagnostics.

:rf.error/fresco-true-child

You let true reach child position.

Named in Views and reads.

:rf.error/ui-tree-malformed

You let a value outside the structural-tree grammar reach an L2 tree or a projection. This is a corpus id rather than a Fresco one: the wider framework defines the spelling and Fresco reuses it.

Fix the template or the runtime value, which the message names.

Reads and the render extent

A subscription read is only meaningful while a boundary body is running, because that is the extent whose read set the runtime is recording. Every complaint here says the read left that extent, or that the body did something a body may not do.

Taught in Views and reads.

:rf.error/fresco-sub-outside-render

You read a subscription outside a boundary body.

Named in Views and reads, Testing, Diagnostics.

:rf.error/fresco-deferred-read-at-boundary

You let an unforced delay reach a boundary's props.

A function is called on every child render, so its reads are the child's edges and are kept.

Named in Views and reads, Testing, Diagnostics.

:rf.error/fresco-generation-fence-exhausted

You wrote to app-db from a body, on four consecutive runs.

A body that writes on every render cannot be fenced.

Frames

A frame is carried, never inferred. When one of these fires, some value crossed a boundary that does not carry the frame with it.

Taught in Events as data.

:rf.error/no-frame-context

You rendered a Fresco boundary whose React context carries no frame. This is a corpus id rather than a Fresco one: the wider framework defines the spelling and Fresco reuses it.

The op fails fast and is NOT routed to a synthesised default; the fix is to carry the frame explicitly (capture it as a value at render time and thread it into the callback, or pass {:frame …}).

Named in Events as data, Interop, Islands, SSR and hydration, Migrating from Reagent.

Intents and callback positions

An intent is a vector that means dispatch this. It only means that where something is prepared to lower it, and these complaints mark the positions where nothing is.

Taught in Events as data.

:rf.error/fresco-intent-outside-boundary

You lowered or fired an intent with no frame-locked dispatch bound.

Named in Diagnostics, Errors.

:rf.error/fresco-intent-needs-the-event

You wrote an event-reading intent at a value-first foreign callback.

The one callback form receives every argument the invoker passed, in order.

Named in Events as data, Interop.

:rf.error/fresco-malformed-prevent

You wrapped something other than exactly one intent vector in the prevent decorator.

Named in Events as data.

Controlled inputs

A controlled field has one owner and one reset trigger. These complaints fire where a second owner was implied, or where the trigger was written at a position that cannot receive it.

Taught in Controlled inputs.

:rf.error/fresco-revision-not-controlled

You put the reset trigger on something that is not a controlled text field.

Named in Controlled inputs, Forms, Diagnostics.

:rf.error/fresco-file-input-value-marker

You read ::h/value off a file input, where .value is the C:\fakepath\ fiction and the first file's name — not the files.

Error boundaries

An error boundary that reports nothing looks exactly like one that never caught anything, so its props are a closed roster rather than a suggestion.

Taught in Errors.

:rf.error/fresco-boundary-unknown-prop

You wrote a key outside h/error-boundary's closed roster — a misspelled :on-error is an error boundary that reports nothing.

:rf.error/fresco-boundary-bad-on-error

You gave h/error-boundary an :on-error that is neither an intent vector nor a function, so nothing could fire it.

Hosts and the raw escape

A defhost declaration is validated once, at the declaration, rather than at every crossing. Most of this group therefore fires at load time and names the declaration; the raw [:>] escape has no declaration to validate, so its one fires at the crossing instead.

Taught in Interop.

:rf.error/fresco-host-no-component

You declared a defhost over nil.

:rf.error/fresco-bad-host-declaration

You wrote a defhost declaration outside its shape, and the reason names which: options that are not a map (usually a docstring written after the component instead of before it); an option outside #{:callbacks :slots :server :fallback} — the retired :ssr spelling included; a :callbacks contract outside :event and :render; a :slots value that is not a set of ordinary prop names (a non-set, an entry that names no prop, key/ref, one slot spelled twice, or a position that is also a declared callback); or a form after the options map, which is discarded rather than merged.

Named in Interop, SSR and hydration.

:rf.error/fresco-host-bad-ssr-policy

You gave a defhost a :server value outside the two it admits, or a :fallback the policy beside it cannot carry.

Named in Interop, SSR and hydration.

:rf.error/fresco-host-fallback-boundary-head

You put a defview or defhost head inside a declared fallback.

Plain hiccup in the fallback, or :server :render to render the real subtree on the server.

Named in Interop, SSR and hydration.

:rf.error/fresco-host-unclaimed-callback

You wrote the one callback form at a defhost position declared a ReactNode slot, where markup lowers and there is no contract to give a function.

Write the markup there, or take the position out of :slots.

Named in Events as data, Interop, Diagnostics.

:rf.error/fresco-raw-not-a-component

You handed the raw escape nil in component position — usually a :default import that resolved nothing — or a Fresco defview or defhost head, which is a head in its own right.

Write [:> Component props & children] with the real component, or write the head as [my-view …]. Any other invalid type is React's own error at render.

Named in Interop.

Routing

A route link is an ordinary anchor that the router owns. These fire where it was asked to be something else.

Taught in Routing and navigation.

You rendered a route link with no ambient frame.

You gave a route link an :on-click outside the route-click roster.

:rf.error/routing-artefact-missing

You rendered a route link with routing absent. This is a corpus id rather than a Fresco one: the wider framework defines the spelling and Fresco reuses it.

Add day8/re-frame2-routing to your dependencies and require re-frame.routing at boot, before frames are constructed.

Named in Routing and navigation.

Motion and presence

A presence tray animates a child out after that child has stopped being rendered, which it can only do if it can still identify the child and knows when to give up.

Taught in Motion and presence.

:rf.error/fresco-presence-child-unkeyed

You gave a presence boundary a child with no :key — a child that is not a hiccup vector included.

:rf.error/fresco-presence-timeout-required

You left a presence boundary's timeout absent or not positive.

Overlays and focus

An overlay positions itself against a trigger you name by DOM id, so the one thing it cannot do is resolve a name to nothing and say nothing.

Taught in Overlays and focus.

:rf.error/fresco-overlay-anchor-missing

You gave an overlay an :anchor naming a DOM id no element in the document carries. Omitting :anchor is legal and silent — a modal takes none, and a popover without one is asking for the default position; this catches the name that resolves to nothing.

Generate a unique, stable trigger id from the instance id, and render the trigger in the same tree as the overlay so the two arrive in one commit.

Ephemeral state

A concern is registered once and keyed by something the domain owns. Both halves are checked at registration and at use.

Taught in Ephemeral state.

:rf.error/fresco-state-bad-argument

You gave reg-state a concern that is not namespace-qualified, or options outside {:default …}; or you used an instance key outside the accepted set (nil included) at a read or a write. The reason names which.

The test kit

L2 renders one body as a semantic tree with no React running. It refuses rather than guesses whenever the thing being asserted is not visible at that level, and the recovery is usually the next level up rather than a different assertion.

Taught in Testing.

:rf.error/fresco-test-not-a-body

You gave an L2 tree form a head that is not a defview body.

:rf.error/fresco-test-not-a-render-form

You gave an L2 tree something other than a hiccup form.

:rf.error/fresco-test-plain-fn-head

You put a plain function in a hiccup head inside an L2 tree.

Named in Diagnostics.

:rf.error/fresco-test-boundary-body-not-retained

You gave an L2 tree a minted head in a build that erased its body.

:rf.error/fresco-test-bad-option

You gave an L2 tree non-map options, or an option outside its closed roster #{:subs}.

:rf.error/fresco-test-bad-reads

You gave an L2 tree a :subs option that is not a query-to-value map.

:rf.error/fresco-test-missing-read-fixture

You let an L2 body read a subscription no fixture answers.

:rf.error/fresco-test-host-is-opaque

You let a defhost crossing reach the L2 semantic tree.

:rf.error/fresco-test-react-is-opaque

You let a raw React element reach the L2 semantic tree.

:rf.error/fresco-test-not-a-host

You read the declared server policy off something that is not a defhost.

:rf.error/fresco-test-not-a-native-form

You gave an L1 projection a form whose head is not a tag keyword.

:rf.error/fresco-test-not-an-intent

You gave the L1 marker materializer something other than an intent vector.

:rf.error/fresco-test-not-a-dom-node

You gave the canonical-DOM comparator something that is not a DOM node.

:rf.error/fresco-test-no-handler-at-position

You fired at a prop position the form does not write.

:rf.error/fresco-test-position-is-not-a-handler

You fired at a position that lowers to something other than a function.

:rf.error/fresco-test-l1-dispatch

You invoked a handler lowered by a pure L1 projection.

Ids that are claimed but not raised

The index above is what the shipped package raises. A few further spellings are claimed without being raised, and knowing which is which saves a fruitless search: none of them can appear in an error you caught, and none of them is a spelling to mint for yourself.

Three rules govern the whole set, and they are the reason an id is worth asserting on in the first place.

  1. An id never changes meaning. If the refusal it names becomes a different refusal, that is a new id and the old one retires.
  2. An id never changes spelling. A rename is a retirement plus a mint, and both are recorded.
  3. A retired id is tombstoned and never reused. A consumer's stored errors, an error monitor's grouping rule and a page of prose all outlive the code, so a reused spelling makes every one of them silently wrong about which failure it saw.

Reserved

Each of these names a refusal this guide already teaches by mechanism, on a surface that is not built yet. The reservation is not bookkeeping: a refusal with no id is invisible to a round trip — nothing raises it and no index carries it, so the raise-set and the index agree while the coverage is entirely missing. Claiming the spelling makes that gap countable, stops two builders minting two names for one refusal, and lets a chapter cite an id today.

A reserved id carries no payload yet; that is settled by the work that writes the emitter, which moves the row up into the index above in the same change. So a reservation is promoted, never drifted into.

Reserved What it will refuse
:rf.error/fresco-view-called-directly a defview invoked as a function instead of mounted as a hiccup head
:rf.error/fresco-test-hook-is-opaque a React hook reached from a body run at L2, where no React is running
:rf.error/fresco-test-native-is-opaque a native-tier element reaching the L2 semantic tree, as host and raw-React elements already do
:rf.error/fresco-contenteditable-not-controllable a controlled :value binding on a contenteditable region
:rf.error/route-link-bad-prefetch a route link's :prefetch carrying a value no link surface accepts — routing raises it, on both hosts, for h/route-link and rf/route-link alike
:rf.error/fresco-route-link-claimed-intent-position a route link supplying :prefetch :intent and a value of its own at :on-mouse-enter, :on-focus or :on-touch-start — the three positions :prefetch claims

Dead

:rf.error/fresco-test-residue-after-quiescence is tombstoned. It was reserved for a raising clean-state assertion on the mounted test kit, and the kit landed choosing to report instead: hm/assert-clean! files residue through the test runner rather than throwing, because residue is a test failure and not a refusal of the instrument. A throw would make the tool that detects a leak indistinguishable from the tool breaking, and would abort at the first finding instead of reporting all of them. So the reservation named a refusal its own surface decided not to have. It was never minted and never raised, so no stored error and no monitor rule anywhere carries it — and by rule 3 above it stays dead rather than being recycled for the next refusal on that surface.