Skip to main content

How a refusal reaches the caller

A beforeChange handler says no. Core writes nothing. It raises one Error report. It throws MutationCancelledError back to the call that started the write.

The Dataset door​

This is the path a programmatic write takes when a beforeChange handler refuses it. The caller is dataset.entries.update, or any other mutator that ends in commitChangeSet. Undo and redo use the same tail.

Derived from data/transaction.ts, data/error-reporting.ts, data/event-bus.ts, model/error-report.ts, model/errors.ts, api/watch-all-errors.ts, api/attempt-mutation.ts.

A beforeChange refusal, caller to output Sequence diagram of a Dataset write that a beforeChange handler refuses: the update call, the veto, the Error report, and the thrown MutationCancelledError. ENTRIES.UPDATE BEFORECHANGE REFUSE(REASON) RETURN FALSE BUILD REPORT EMIT ERROR THROW CANCEL APP Caller entries.update DATA Dataset commitChangeSet EVT Handler beforeChange OUT Subscriber error event LEGEND handler · refuse call return notify
A beforeChange veto inside commitChangeSet. Coral is the refuse() call. The open dashed arrow is the error event. The filled dashed arrow back to the caller is the throw.
dataset.on('beforeChange', ({ refuse }) => refuse('t1 is locked.'));
dataset.entries.update('t1', { name: 'Framing' });
// throws MutationCancelledError
// Dataset 'error' fires when something is subscribed

Each step​

Time runs down the diagram. The store never applies the changeset. The two outputs are the throw and, when a listener exists, the Error report.

Derived from data/transaction.ts, data/error-reporting.ts, data/event-bus.ts, model/errors.ts.

StepWhat runsWhat it does
ENTRIES.UPDATEentries.update → runTransaction → commitChangeSetThe body stages the edit. The extension hook and the Rollup run. Core builds one changeset, then asks beforeChange.
BEFORECHANGEbus.emit('beforeChange', { changeSet, refuse })Core makes one RefusalNote per emit and puts refuse on the payload. Every handler runs. A veto from one handler does not skip the rest.
REFUSE(REASON)refuse('t1 is locked.')The call writes the words onto the note and returns false. The first reason wins. Two handlers never join their sentences. A bare false still refuses, with no reason.
RETURN FALSEemit → falseCore discards the write set. Nothing is stored. No change event fires.
BUILD REPORTbuildRefusalReport + MutationCancelledErrorOne builder, in data/error-reporting.ts. For this door the message is the thrown error's own wording. Severity is info. by is consumer.
EMIT ERRORraiseErrorOn(dataset.bus, report)The report reaches a listener on the Dataset error event. If nobody listens, this step does nothing. This site has no console fallback. A Gantt does not forward Dataset reports — two Gantts on one Dataset would each hear the same report twice. Call watchAllErrors([dataset, gantt], …) to hear both buses.
THROW CANCELthrow MutationCancelledErrorThis output always happens. The error carries the refused changeset and the reason. attemptMutation(() => …) catches it and returns false, so a button does not need its own try/catch. Any other error still throws.

The other doors​

Three vetoes share buildRefusalReport. The cell editor raises its own report. A silent canWrite refusal is not this flow: no handle paints, and no report fires.

Derived from view/gesture-pipeline.ts, view/live-region.ts, view/capability.ts, extensions/features/inline-editing.ts, api/gantt.ts.

DoorWho says noThrowReport busLive region
beforeChangea Dataset handlerMutationCancelledErrorDataset errorno — subscribe with watchAllErrors
beforeEntryMove / beforeEntryResizea Gantt handlernone — the pipeline restores the previewGantt error, codes entry-move-cancelled / entry-resize-cancelledyes — LiveRegion is already subscribed
row or bar Deletethe remove rule (a locked Entry, or a parent that holds one)none — the command removes nothingGantt error, code entry-remove-refusedyes — same Gantt bus
"Clear dates"a lock, a writable rule, or beforeChangenone — the command writes nothingGantt error, code entry-clear-dates-refusedyes — same Gantt bus
vertical dropa rule (reorder, a lock rule, a place rule)none — the drop writes nothingGantt error, code entry-drop-refused, raised on releaseyes — same Gantt bus
cell editorthe inlineEditing() pluginnone — the editor stays open, or a notice sits on the cellGantt error, plugin codes such as derived-valueyes — same Gantt bus

:::note A refused gesture commit is one record, not two The pipeline reports a silent beforeEntryMove / beforeEntryResize veto. When the gesture passes that gate and then beforeChange refuses the write, data/transaction.ts already raised the Dataset report. commitEntryEdits returns false, and the pipeline does not raise a second one. :::

A drag that a beforeEntryMove handler refuses uses the same refuse(reason) call. Core builds the sentence itself, because that door has no thrown error to quote:

gantt.on('beforeEntryMove', (move) =>
move.shiftsTime && move.start < mobilization
? move.refuse('The drop is before mobilization.')
: undefined,
);

The report message is Nothing was saved. A beforeEntryMove handler refused this move and said: "…". A bare false drops the and said clause.

What the report carries​

An Error report is the record a consumer subscribes to. A FreeGanttError is the class a consumer catches. They are two different things. Core raises reports and keeps none.

Derived from model/error-report.ts, data/error-reporting.ts, view/live-region.ts, api/watch-all-errors.ts, harness/main.ts, harness/e2e/editing.ts, harness/e2e/data.ts.

FieldOn a beforeChange refusal
atstamped by the raiser from time/'s now()
codemutation-cancelled
messagethe thrown error's sentence, including the handler's words when they called refuse
severityinfo — the library said no on purpose
byconsumer — the bus saw a handler return false
reasonthe handler's own words, or absent after a bare false
causethe MutationCancelledError, which still holds the changeset

LiveRegion reads the Gantt error event and copies report.message into a polite live region for info and error. It stays silent on warning. A Dataset refusal never reaches that node on its own.

The harness demos call watchAllErrors([dataset, gantt], …) and log each report. That is how a Dataset refusal becomes a line in the demo log.