Ensure every async boundary fully captures both rejected promises and synchronous exceptions, and only performs recovery/retry when the error classification is precise and safe.
Core rules: 1) Always cover “throw inside then”
.then(fnSuccess, fnFail), remember fnSuccess/attach may throw; you still need a .catch(...) that routes all failures.fail() path (not an unhandled rejection).2) Never leave detached promises unobserved
.catch(...) to prevent unhandled rejections and to keep UI/state honest (don’t show “success” before the async work succeeds).3) Clear timers and state on failure
4) Retry only with proven semantics
5) Fail fast on invariant violations that risk side effects
Example pattern (covers throw + maps to controlled failure):
// Bad: throw inside attach() can bypass the rejection handler.
void loadMxc().then((mxc) =>
mxc.spawn(/*...*/).then((sandboxed) => handle.attach(sandboxed), fail)
);
// Good: one catch for all async failures, including throws.
void loadMxc()
.then((mxc) => mxc.spawn(/*...*/))
.then((sandboxed) => handle.attach(sandboxed))
.catch((error) => {
handle.fail(new Error(`Sandboxing failed: ${String(error)}`));
});
Example pattern (detached promise must be caught):
this.say('checkpoint_created');
void this.checkpointTracker.commit()
.then((hash) => this.messageStateHandler.updateClineMessage(/*...*/))
.catch((error) => {
// log + optionally update UI to reflect failure
console.error('Failed to create checkpoint:', error);
});
Checklist for PRs in this area:
.then(...) path has a downstream .catch(...) (or uses try/catch around awaits)..catch.