thefrontside / thefrontside/effection
v4.1.0: task.halt() rejects with the task's own error when the halt lands during a successful cleanup
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 856
- Forks
- 39
- Avg merge
- 2d 8h
- Merged PRs (30d)
- 8
Description
Summary
When a task's body fails and its cleanup then runs successfully, task.halt() rejects with the body's
error if the halt is requested while that cleanup is still in flight. The teardown did not fail.
This conflicts with the documented contract for Task.halt(), which says errors raised by it
"only represent problems that occured during the teardown", and that halt can succeed even if the task failed.
The same task failure, with the same successful cleanup, is reported differently depending only on when the halt
is requested, so a consumer cannot use halt() to tell "your teardown failed" from "your task failed".
Reproduction
// effection 4.1.0
import { run, spawn, scoped, ensure, until, sleep, suspend } from 'effection';
const deferred = () => { let resolve; const p = new Promise((r) => { resolve = r; }); return { promise: p, resolve }; };
async function probe(label, body, { awaitCleanupFirst, gated }) {
const cleaning = deferred();
const gate = deferred();
let halt = 'resolved';
await run(function* () {
const task = yield* spawn(() => body(cleaning, gate));
// SUBJECT: the body has already failed and its cleanup is running when the halt is asked for.
// CONTROLS: the task is alive; its cleanup begins because of the halt.
if (awaitCleanupFirst) yield* until(cleaning.promise); else yield* sleep(5);
if (gated) setTimeout(() => gate.resolve(), 20);
try { yield* task.halt(); } catch (e) { halt = `REJECTED: ${e.message}`; }
yield* sleep(10);
});
console.log(`${label.padEnd(46)} halt() -> ${halt}`);
}
// The body fails; its cleanup SUCCEEDS; the halt lands while that cleanup runs.
const subject = (cleaning, gate) => scoped(function* () {
yield* ensure(function* () { cleaning.resolve(); yield* until(gate.promise); });
throw new Error('BODY_FAILED');
});
// Cancelling a healthy task whose cleanup succeeds.
const healthy = (cleaning, gate) => scoped(function* () {
yield* ensure(function* () { cleaning.resolve(); yield* until(gate.promise); });
yield* suspend();
});
// A genuine teardown failure.
const badCleanup = (cleaning) => scoped(function* () {
yield* ensure(function* () { cleaning.resolve(); throw new Error('CLEANUP_FAILED'); });
yield* suspend();
});
await probe('SUBJECT body fails, cleanup OK, halt mid-cleanup', subject, { awaitCleanupFirst: true, gated: true });
await probe('CONTROL healthy task, cleanup OK, cancelled', healthy, { gated: true });
await probe('CONTROL genuine teardown failure', badCleanup, { gated: false });
Observed
effection 4.1.0, node v24.6.0
SUBJECT body fails, cleanup OK, halt mid-cleanup halt() -> REJECTED: BODY_FAILED
CONTROL healthy task, cleanup OK, cancelled halt() -> resolved
CONTROL genuine teardown failure halt() -> REJECTED: CLEANUP_FAILED
Both controls behave as documented. Only the subject reports a non-teardown error, and its cleanup ran to completion.
Expected
halt() resolves in the subject case. The body's failure belongs to the task's own outcome, not to the halt.
Why it matters
We use halt() in a supervisor that replaces one long-running operation with another. A teardown failure means a
native resource was not released and nothing may run on it again, so it ends the session; an ordinary failure in the
operation is recoverable and the next operation should start. Because an ordinary failure racing a cancellation is
reported through halt(), the supervisor tears down a healthy session. There is no other signal to distinguish them:
the error arrives by the same route, in the same phase, and carries nothing that separates it.
Environment
- effection 4.1.0
- Node v24.6.0, macOS
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at Task.halt() and trace how a task body error is propagated while ensure cleanup is in flight. Reproduce the subject and two controls from the issue with effection 4.1.0 and Node v24.6.0; done means the subject's halt resolves, healthy cancellation still resolves, and a genuine teardown failure still rejects.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- developer-experience
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100