Bug Report Checklist
Expected
Summary
PR #5612 made Suite.prototype.timeout(ms) retroactively propagate the new timeout to already-registered children, fixing #5422. However, it only iterates over this.tests and this.suites — it misses the hook arrays (_beforeAll, _beforeEach, _afterAll, _afterEach).
As a result, the chained form
describe('foo', () => {
before(async () => { /* slow setup */ });
it('bar', () => { /* ... */ });
}).timeout(10000);
does not raise the timeout for before (or any other hook). The hooks were created during the describe callback with the suite's timeout at that moment (default 2000ms), and the subsequent .timeout(10000) call never reaches them.
Actual
Which call shapes work / don't work
| Form |
before / after |
it |
describe('x', function () { this.timeout(N); before(...); it(...); }) |
✅ works (hook reads suite timeout at _createHook time) |
✅ works |
describe('x', () => { before(...); it(...); }).timeout(N) (chained, after callback returns) |
❌ broken — hooks were created with default 2000ms, setter doesn't propagate to them |
✅ works (PR #5612) |
describe('x', () => { before(function () { this.timeout(N); ... }); }) (per-hook override) |
✅ works |
n/a |
Root cause
lib/suite.mjs, Suite.prototype.timeout setter (master, lines 222–233):
this._timeout = parseInt(ms, 10);
// Allow overriding inner/nested suites
// https://github.com/mochajs/mocha/issues/5422
for (const t of this.tests) {
t.timeout(this._timeout);
}
for (const s of this.suites) {
s.timeout(this._timeout);
}
return this;
The four hook arrays — this._beforeAll, this._beforeEach, this._afterAll, this._afterEach — are populated by the time the chained .timeout(N) runs (the describe callback already finished), but the setter doesn't visit them.
Minimal, Complete and Verifiable Example
Reproduction
// test/repro.test.js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// A — BROKEN: chained .timeout() after describe doesn't reach before()
describe('A: chained .timeout() — does it reach before()?', () => {
before(async () => {
await sleep(3000); // > default 2000ms hook timeout, < chained 10000ms
});
it('reaches this only if before() was rescued by chained .timeout(10000)', () => {});
}).timeout(10000);
// B — OK: chained .timeout() does reach it() (PR #5612)
describe('B: chained .timeout() — does it reach it()?', () => {
it('it() sleeps 3000ms', async () => {
await sleep(3000);
});
}).timeout(10000);
// C — OK: function() form, this.timeout() inside the describe callback
describe('C: function() form, this.timeout() inside', function () {
this.timeout(10000);
before(async () => {
await sleep(3000);
});
it('passes', () => {});
});
$ npx mocha test/repro.test.js
A: chained .timeout() — does it reach before()?
1) "before all" hook for "reaches this only if ..."
B: chained .timeout() — does it reach it()?
✔ it() sleeps 3000ms (3000ms)
C: function() form, this.timeout() inside
✔ passes
2 passing (8s)
1 failing
1) A: chained .timeout() — does it reach before()?
"before all" hook for "...":
Error: Timeout of 2000ms exceeded.
Suggested fix
Add hook propagation alongside tests / suites:
for (const hooks of [this._beforeAll, this._beforeEach, this._afterAll, this._afterEach]) {
for (const h of hooks) {
h.timeout(this._timeout);
}
}
Hooks are Runnable instances and already have a .timeout(ms) setter, so this mirrors the existing tests/suites loops exactly.
Versions
Affects: mocha master (current lib/suite.mjs), confirmed reproducing on 11.7.6.
Environment
- mocha 11.7.6 (also reproduced against current
master lib/suite.mjs)
- node v24.11.1
- macOS 25.5.0 (Darwin arm64)
Additional Info
No response
Bug Report Checklist
npm audit, or GitHub Advisory issue.faqlabel, but none matched my issue.Expected
Summary
PR #5612 made
Suite.prototype.timeout(ms)retroactively propagate the new timeout to already-registered children, fixing #5422. However, it only iterates overthis.testsandthis.suites— it misses the hook arrays (_beforeAll,_beforeEach,_afterAll,_afterEach).As a result, the chained form
does not raise the timeout for
before(or any other hook). The hooks were created during the describe callback with the suite's timeout at that moment (default 2000ms), and the subsequent.timeout(10000)call never reaches them.Actual
Which call shapes work / don't work
before/afteritdescribe('x', function () { this.timeout(N); before(...); it(...); })_createHooktime)describe('x', () => { before(...); it(...); }).timeout(N)(chained, after callback returns)describe('x', () => { before(function () { this.timeout(N); ... }); })(per-hook override)Root cause
lib/suite.mjs,Suite.prototype.timeoutsetter (master, lines 222–233):The four hook arrays —
this._beforeAll,this._beforeEach,this._afterAll,this._afterEach— are populated by the time the chained.timeout(N)runs (the describe callback already finished), but the setter doesn't visit them.Minimal, Complete and Verifiable Example
Reproduction
Suggested fix
Add hook propagation alongside tests / suites:
Hooks are
Runnableinstances and already have a.timeout(ms)setter, so this mirrors the existing tests/suites loops exactly.Versions
Affects: mocha
master(currentlib/suite.mjs), confirmed reproducing on 11.7.6.Environment
masterlib/suite.mjs)Additional Info
No response