mrdoob/three.js · error · Error
transform stack not 0
Error message
transform stack not 0
What it means
Assertion thrown by ctx.validateTransformStack() in the canvas-wrapper polyfill when the internal save/restore stack is not back to zero. It is a post-condition check: after all drawing for a diagram/frame, every save() must have been balanced by a restore(). A non-zero length means at least one save() was never matched.
Source
Thrown at manual/resources/canvas-wrapper.js:180
d.a = m11;
d.b = m12;
d.c = m21;
d.d = m22;
d.e = dx;
d.f = dy;
setTransform( m11, m12, m21, m22, dx, dy );
};
}( ctx.setTransform.bind( ctx ) );
ctx.currentTransform = new DOMMatrix();
ctx.validateTransformStack = function () {
if ( stack.length !== 0 ) {
throw new Error( 'transform stack not 0' );
}
};
return ctx;
}
function wrap( ctx ) {
//patchDOMMatrix();
return patchCurrentTransform( ctx );
}
return {
wrap: wrap,View on GitHub (pinned to da05705fa3)
Solutions
- Make each save()/restore() pair surround a single try/finally block so restore() always runs.
- Find the unbalanced save by bisecting: log stack.length before each save and again at validateTransformStack().
- Ensure no draw helper performs a save() without a guaranteed matching restore() on every branch.
Example fix
// before
function draw( ctx ) {
ctx.save();
ctx.translate( x, y );
paint( ctx ); // if paint throws, restore is skipped
ctx.restore();
}
ctx.validateTransformStack(); // throws: stack not 0
// after
function draw( ctx ) {
ctx.save();
try {
ctx.translate( x, y );
paint( ctx );
} finally {
ctx.restore();
}
} Defensive patterns
Strategy: validation
Validate before calling
// Mirror the polyfill's stack depth and assert balance before validateTransformStack().
let depth = 0;
const save = () => { ctx.save(); depth++; };
const restore = () => { ctx.restore(); depth--; };
function assertBalanced() {
if ( depth !== 0 ) throw new Error( `unbalanced transforms: depth ${depth}` );
} Try / catch
try {
ctx.validateTransformStack();
} catch ( err ) {
if ( /transform stack not 0/.test( err.message ) ) {
console.warn( 'leaked save(); draining remaining restores' );
try { while ( true ) ctx.restore(); } catch { /* stack drained */ }
} else throw err;
} Prevention
- Use try/finally around every save()/restore() region.
- Run validateTransformStack() in a debug build to catch leaks early.
- Log stack depth at function boundaries to localize the unmatched save().
When it happens
Trigger: Calling validateTransformStack() after a draw routine that executed ctx.save() but returned or threw before its matching ctx.restore(). Used by lesson diagram code to catch save/restore leaks between frames.
Common situations: A new code path (early return, thrown error) between save() and restore(). A diagram's draw function that conditionally calls save() but unconditionally calls restore() elsewhere, or vice versa. Migrating a snippet that relied on the browser resetting the stack per-frame (this polyfill does not).
Related errors
- "transform stack empty!
- no diagram ${name}
- no diagram ${name}
- no primitive ${name}
- no ui for ${primitiveName}:${ndx} param: ${name}
AI-assisted analysis of mrdoob/three.js@da05705fa3 (2026-08-12).
Data as JSON: /api/errors/5e995d49d9b3e634.
Report an issue: GitHub.