Cancellation
Every execute is handed an AbortSignal. Forward it to whatever you call and abandoned work actually stops:
.task({
name: "fetchQuote",
execute: async ({ input, signal }) => {
const response = await fetch(`/quotes/${input.sku}`, { signal });
return response.json();
},
})The signal is aborted the moment that attempt is over, whichever comes first:
- its
timeouttripped, - the run's own signal was aborted,
- or
executereturned, and the attempt is simply done.
Only execute gets one. A rollback is what you want after a cancel, so compensate is never the thing being cancelled.
Cancelling a run
execute takes a signal of its own:
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000);
const result = await saga.execute(input, { signal: controller.signal });Abort it and the saga stops before its next task and rolls back everything it already did. You get the usual success: false result, carrying the abort reason as error; execute still does not reject.
failedTask is left undefined: a cancellation is not a task's fault.
Retries stop too
A task mid-retry does not finish out its attempts. Once the run's signal is aborted, the failed attempt is the last one: no backoff wait, no further tries, and the abort reason propagates instead of the task's error. Retrying work the caller has already given up on is the thing an abort exists to prevent.
What it cannot do
The signal is a request, not a kill switch. A task that ignores it runs to completion, and the saga waits for it before rolling back; JavaScript has no way to stop a promise from the outside.
That matters most for the two places minisagas stops waiting on its own:
- a
timeout, which gives up on an attempt, and failFast, which gives up on the sibling branches.
In both, work that ignores its signal keeps going, and its effects are never compensated, because as far as the saga knows it never finished. Honour the signal in anything that writes.
Retries get a fresh signal each attempt
Attempt 2 does not inherit attempt 1's aborted signal, and attempt 1's is aborted as soon as that attempt fails rather than after the backoff wait. Read signal from the arguments each time rather than closing over it.