Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,11 @@ _Avoid_: Stuck call, dangling call, no-op path.

> **Maintainer:** "If the **MutationFn** throws, does the **Call** end?"
>
> **Designer:** "No — the **Trigger** swallows the throw, **pending**
> clears, the **Call** stays open. The **MutationFn** itself decides
> when to invoke `call.end()`."
> **Designer:** "No — the **Call** stays open because closing needs an
> explicit `call.end()`, and **pending** clears via `.finally`. The
> **Trigger** doesn't catch the throw; an uncaught one escapes as an
> unhandled rejection (ADR-0016), so the **MutationFn** handles its own
> errors and decides when to invoke `call.end()`."
>
> **Maintainer:** "And if the caller never provides a **MutationFn**?"
>
Expand Down
4 changes: 4 additions & 0 deletions docs/adr/0016-mutation-flow-propagates-throws.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ The Trigger returned by `useMutationFlow` lets a rejected `mutationFn` propagate
- **Two tests in `packages/react-call/src/__tests__/mutation-flow.test.tsx` need an unhandled-rejection suppressor.** The test "thrown mutationFn keeps the dialog open and clears pending" and the first half of "Callable.update can swap the mutationFn between submits" intentionally throw to assert the UX contract. The assertions stay valid — what changes is that the rejection now escapes the trigger and would fail the test runner. Scoped to the relevant `describe`s via `beforeEach`/`afterEach` listeners on the test runtime's unhandled-rejection event; not applied globally, so unintended unhandled rejections in other tests still fail.
- **Bundle size goes down by a few bytes** in `dist/mutation-flow.js`; the existing `size-limit` budget gains margin. `dist/main.js` is unaffected — the hook ships from the `react-call/mutation-flow` subpath.
- **Not labeled BREAKING in the changeset.** `useMutationFlow` only exists from `2.0.0-next.3`; the stable 2.0 has not shipped. Early-adopter consumers on the `next` tag who rely on silent failures will notice a console warning at first run; the migration is "wrap your `mutationFn` body in `try/catch` if the failure matters to your UX or telemetry". Documented in the 2.0 changelog as a regular behavior note, not as a breaking change against the 1.x line (which never had the hook).

## Later correction (2026-07-16)

The "`CONTEXT.md` is unchanged" consequence above was inaccurate: only the glossary *entries* were neutral. The file's *example dialogue* still framed the Trigger as "swallowing the throw", and that residual wording later propagated into the published skill (`skills/react-call/references/mutation-flow.md`). Both have since been reframed to the `call.end()`-grounded explanation this ADR established.
9 changes: 7 additions & 2 deletions skills/react-call/references/mutation-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ await Confirm.call({
})
```

If the MutationFn throws, the Trigger swallows it, `pending` clears, and the Call
**stays open** — the handler itself owns `call.end()`.
The Call **stays open** on failure because closing requires an explicit
`call.end()` — a MutationFn that returns (or throws) without reaching `end`
leaves the Call in the Stack, ready to retry. `pending` always clears (via
`.finally`). The Trigger does **not** catch errors: an uncaught throw escapes
as an unhandled promise rejection. Handle failures **inside** the MutationFn —
`try/catch`, show your error UI, then `return` without calling `end` — rather
than letting it throw.

## Optional MutationFn + `.orEnd`

Expand Down
Loading