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

  1. Make each save()/restore() pair surround a single try/finally block so restore() always runs.
  2. Find the unbalanced save by bisecting: log stack.length before each save and again at validateTransformStack().
  3. 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

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


AI-assisted analysis of mrdoob/three.js@da05705fa3 (2026-08-12). Data as JSON: /api/errors/5e995d49d9b3e634. Report an issue: GitHub.