Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

Failure — what went wrong the last time an action ran

Failure.Any · Failure.Message · Failure.CorrelationId · Failure.Action · Failure.Dismiss

When an action fails — a security denial, a constraint violation, a server that refused — the platform records why, and `Failure` is how a page says so. `Failure.Any` is the flag a surface binds its visibility to, `Failure.Message` carries the words of whatever refused it, and `Failure.Dismiss` clears it. It is a read surface bound into every render scope, needs no `using`, and has no appearance of its own: whether the message becomes a banner, a toast or an inline note is yours to draw.

stable3 examples compiled by CIuierrorfailurefeedback

Summary#

An action can fail. A security rule refuses the write, a constraint rejects the row, the server is unreachable — and without somewhere to put that, the person who pressed the button sees a button that did nothing. Failure is where the platform puts it:

component SaveBar() {
  render {
    if (Failure.Any) { Alert(Failure.Message, tone: Tone.Danger); }
  }
}

Failure is bound into every render scope and is part of the core baseline, so it needs no using and nothing is declared to switch it on. It is filled by the runtime whenever an action fails, and it holds the most recent failure until something clears it.

Signature#

Failure.Any             // bool   — did the last action fail, and has nobody dismissed it yet?
Failure.Message         // string — what went wrong, in the words of whatever refused it
Failure.CorrelationId   // string — the id to paste into `osy logs --correlation <id>`
Failure.Action          // string — the action that failed, as `ui:<Component>.<Action>`
Failure.Dismiss         // Action — clear it; `Failure.Any` goes false

Description#

What fills it, and in whose words#

Any action that fails fills it — an onPress, an onChange, a lifecycle hook, anything the page RUNS. The message is taken from whatever actually refused the call, in that thing's own words, because those are the words that say something useful:

what failedwhat Failure.Message reads
a security rulethe rule's own sentence — "Create of 'Invoice' denied — no create rule is active for this caller…"
a constraint or an invariantthe author's own message from the declaration
a throw in your functionthe exception's message
the transport, and only thenthe HTTP wording — "POST /fn/save failed: 503"

The transport wording is the last resort rather than the first. A denial that reached the user as 403 would be technically true and useless; the rule that refused it already wrote a sentence meant for a person, and that is the one that surfaces.

Showing it once, not once per frame#

Failure.Any is a flag, not a stream. It is raised once per cause and stays raised until dismissed or until the next failure replaces it — so a surface bound to it appears once and stays, rather than four copies arriving a second apart. Binding a banner's visibility straight to Failure.Any is therefore the whole of the work.

Clearing it, so a banner is not permanent#

Failure.Dismiss is a verb the platform supplies, not one you write: the value lives in the client runtime, so only the runtime can clear it. It is declared as an Action, which means it goes wherever an action is wanted — onPress: Failure.Dismiss — and needs no wrapper:

component DismissibleFailure() {
  render {
    if (Failure.Any) {
      Row(gap: 8, align: Align.Center) {
        Alert(Failure.Message, tone: Tone.Danger);
        Button("Dismiss", onPress: Failure.Dismiss, tone: Tone.Ghost);
      }
    }
  }
}

Without a dismiss the banner persists until the next navigation, which is a reasonable default and a poor experience on a page the user then goes on working in.

Giving the user something to report#

Failure.CorrelationId is the id that ties this failure to its server-side log line. Surfacing it rather than only logging it is worth the pixels: a failure a user cannot report is most of the way to a failure nobody can fix, and osy logs --correlation <id> turns a screenshot into a diagnosis.

component FailureNote() {
  render {
    if (Failure.Any) {
      Stack(gap: 4) {
        Text(Failure.Message, fontWeight: "600");
        Text("Reference: " + Failure.CorrelationId, fontSize: "0.85rem", opacity: "0.7");
      }
    }
  }
}

Failure.Action names the action that failed, as ui:<Component>.<Action> — for a surface that wants to say which verb it was, or to show a failure only for one particular action.

Three failure surfaces, and which one you want#

Three things can go wrong and they are deliberately separate, because replacing the whole screen for a failed button would be a lie about what broke:

what brokethe surfacewhat the user sees
an action on a page that rendered fineFailure — this pagethe page still works; a message says the action did not
a query a region readsA failing querythat region shows the failure in place; the rest of the page is fine
the page itself failed to loadapp.Ui.ErrorSurface (UI surfaces (app.Ui))the page is replaced

Field-level rejections are different again: a ValidationException carries per-field violations you place beside the offending field, which is Validation. Failure is the catch-all for the action as a whole.

What is drawn when you draw nothing#

A page that reads Failure nowhere is not left silent: the platform shows a plain notice at the foot of the screen — the message, the correlation id and a Dismiss — so a refused action never looks like a button that did nothing. It is plain HTML with osy- classes (.osy-action-failed, -message, -correlation, -dismiss), restyled in the app's CSS. While it is up the page gets room at its foot as tall as it, and the row of the control that was pressed is scrolled clear of it, so it never covers what it is about.

Reading Failure anywhere takes the surface over: the platform's notice stands down for the whole page, and whether the failure becomes a banner, a toast, a line under the form or nothing at all is the app's to decide.

A save that lost a race on a [ConcurrencyCheck] row gets a notice of its own — what you wrote, what the row holds now, Keep theirs and Apply mine: The conflict notice — when a page's save meets somebody else's change.

See also#

Related

A failing query

When a query a region reads fails or is refused, that region shows the failure in place — the server's own sentence…

UI surfaces (app.Ui)

`app.Ui` nominates the app's own components for the "system surfaces" the platform would otherwise render a bare…

Pending

When a control's action waits on the server, the platform shows a busy spinner and disables the control — but only…

Validation

You declare a field's rules once, on the entity — `[Required]`, `[Pattern]`, `[MaxLength]` — and give each rule the…

Connection

The live state of the browser's link to the server, and the two verbs that recover it. A component you nominate as your…