discourse/discourse · error · Error
on.cleanup can only be called once
Error message
on.cleanup can only be called once
What it means
A helper defined via helperFn attempted to register a second cleanup callback within a single recomputation. The `on` object passed to the helper's callback exposes `on.cleanup(fn)` for scheduling teardown, and the class stores at most one such function (this.cleanupFn); calling it again before compute() runs again trips this guard. Almost always means the helper body calls on.cleanup inside a loop or in multiple branches that both execute.
Solutions
- Restructure the helper so on.cleanup is called exactly once per compute invocation; combine multiple teardown steps into a single function passed to one on.cleanup call.
- If you need to clean up several resources, collect them in a local array and pass one cleanup that iterates it.
- If you intended a different lifecycle, register a custom destructor with @ember/destroyable (registerDestructor/unregisterDestructor) instead of on.cleanup.
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at frontend/discourse/app/helpers/helper-fn.js:35 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of discourse/discourse@1b2d7253e8 (2026-08-26).
Data as JSON: /api/errors/79554fee2e60dc99.
Report an issue: GitHub.
Appendix: source
Thrown at frontend/discourse/app/helpers/helper-fn.js:35
constructor() {
super(...arguments);
registerDestructor(this, this.cleanup);
}
compute(positional, named) {
if (positional.length) {
throw new Error(
"Positional arguments are not permitted for helperFn-defined helpers. Use named arguments instead."
);
}
this.cleanup();
const on = {
cleanup: (fn) => {
if (this.cleanupFn) {
throw new Error("on.cleanup can only be called once");
}
this.cleanupFn = fn;
},
};
return callback(named, on);
}
@bind
cleanup() {
this.cleanupFn?.();
this.cleanupFn = null;
}
};
}
View on GitHub (pinned to 1b2d7253e8)