name: wrdn-effect-promise-exit description: Replace React/effect-atom mutation handlers that use promise-mode plus try/catch with promiseExit and explicit Exit handling. Use when lint or review flags try/catch around useAtomSet mutation calls, especially UI handlers that set error/busy state after a failed mutation. allowed-tools: Read Grep Glob Bash
You fix one pattern: a React handler awaits an effect-atom mutation in mode: "promise" and catches failures with try/catch.
The preferred UI boundary is mode: "promiseExit" plus Exit.isFailure. This keeps mutation failures as values, matches Effect's error model, and prevents optimistic mutation cleanup from depending on thrown exceptions.
Trace before changing
- Find the mutation setter. Look for
const doX = useAtomSet(<mutationAtom>, { mode: "promise" }). - Confirm it is an effect-atom mutation boundary. The setter should come from
@effect/atom-reactand a mutation atom from./atoms,../api/atoms, or plugin React atoms. - Find thrown-control handling. The same handler has
try { await doX(...) } catch (e) { ... }, usually setting error text, resettingadding/saving, or showing a toast. - Check for non-mutation async work in the same block. If the block also awaits follow-up mutations, convert those to
promiseExittoo or keep a narrow boundary only around truly non-effect APIs. - Do not rewrite unrelated local async code. Probe requests, OAuth popup helpers,
fetch, and browser APIs may need a different skill unless the lint finding specifically points at the mutation call.
Fix shape
- Change the setter to
{ mode: "promiseExit" }. - Import
* as Exit from "effect/Exit"if missing. - Import
* as Option from "effect/Option"only when extracting an optional error. - Replace
try/catcharound the mutation with:const exit = await doX(args);if (Exit.isFailure(exit)) { ...; return; }- success work after the failure branch.
- Use
Exit.findErrorOption(exit)when preserving an existing error message or typed error branch. - Keep existing typed error handling when present, e.g.
SecretInUseError,ConnectionInUseError.
Bad
const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" });
const handleAdd = async () => {
setAdding(true);
setAddError(null);
try {
await doAdd({
params: { scopeId },
payload,
reactivityKeys: sourceWriteKeys,
});
props.onComplete();
} catch (e) {
setAddError(e instanceof Error ? e.message : "Failed to add source");
setAdding(false);
}
};
Good
import * as Exit from "effect/Exit";
import * as Option from "effect/Option";
const doAdd = useAtomSet(addGraphqlSource, { mode: "promiseExit" });
const handleAdd = async () => {
setAdding(true);
setAddError(null);
const exit = await doAdd({
params: { scopeId },
payload,
reactivityKeys: sourceWriteKeys,
});
if (Exit.isFailure(exit)) {
const error = Exit.findErrorOption(exit);
setAddError(
Option.isSome(error) && error.value instanceof Error
? error.value.message
: "Failed to add source",
);
setAdding(false);
return;
}
props.onComplete();
};
Follow-up mutation chains
If success work depends on the mutation result, read it after the failure branch:
const exit = await doAdd(args);
if (Exit.isFailure(exit)) {
setAdding(false);
return;
}
const sourceId = exit.value.namespace;
If a follow-up effect-atom mutation can fail and the UI treats that as add failure, make that setter promiseExit too and branch the same way. Do not put the follow-up mutation in try/catch just because the first mutation now returns Exit.
What not to report
try/catcharound non-effect APIs such asnew URL,JSON.parse, rawfetch, or browser popup code. Those may be real lint findings, but they need a different remediation skill.useAtomSet(..., { mode: "promise" })with no local failure handling and no lint finding. Some call sites intentionally let callers decide the boundary.- Tests or SDK/server Effect code. This skill is for React/effect-atom UI mutation handlers.
- Manual optimistic placeholder cleanup. Use
wrdn-effect-atom-optimisticfor that; if both patterns appear together, fix optimistic plumbing first, then usepromiseExitfor the remaining mutation boundary.
Output requirements
When reviewing, report:
- File and line of the
useAtomSet(..., { mode: "promise" })ortry/catch. - Mutation being called.
- Why it should return
Exitat this UI boundary. - Fix: the exact setter mode and the failure branch to add.
When editing, keep changes local to the handler and imports unless a follow-up mutation in the same success path must also become promiseExit.