{"record":{"id":"5e995d49d9b3e634","repo":"mrdoob/three.js","slug":"transform-stack-not-0","errorCode":null,"errorMessage":"transform stack not 0","messagePattern":"transform stack not 0","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"manual/resources/canvas-wrapper.js","lineNumber":180,"sourceCode":"\t\t\t\td.a = m11;\n\t\t\t\td.b = m12;\n\t\t\t\td.c = m21;\n\t\t\t\td.d = m22;\n\t\t\t\td.e = dx;\n\t\t\t\td.f = dy;\n\t\t\t\tsetTransform( m11, m12, m21, m22, dx, dy );\n\n\t\t\t};\n\n\t\t}( ctx.setTransform.bind( ctx ) );\n\n\t\tctx.currentTransform = new DOMMatrix();\n\n\t\tctx.validateTransformStack = function () {\n\n\t\t\tif ( stack.length !== 0 ) {\n\n\t\t\t\tthrow new Error( 'transform stack not 0' );\n\n\t\t\t}\n\n\t\t};\n\n\t\treturn ctx;\n\n\t}\n\n\tfunction wrap( ctx ) {\n\n\t\t//patchDOMMatrix();\n\t\treturn patchCurrentTransform( ctx );\n\n\t}\n\n\treturn {\n\t\twrap: wrap,","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/mrdoob/three.js/blob/da05705fa31f02710c19772a2aa70c7454807f0c/manual/resources/canvas-wrapper.js#L162-L198","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nfunction draw( ctx ) {\n  ctx.save();\n  ctx.translate( x, y );\n  paint( ctx );           // if paint throws, restore is skipped\n  ctx.restore();\n}\nctx.validateTransformStack(); // throws: stack not 0\n\n// after\nfunction draw( ctx ) {\n  ctx.save();\n  try {\n    ctx.translate( x, y );\n    paint( ctx );\n  } finally {\n    ctx.restore();\n  }\n}","handlingStrategy":"validation","validationCode":"// Mirror the polyfill's stack depth and assert balance before validateTransformStack().\nlet depth = 0;\nconst save = () => { ctx.save(); depth++; };\nconst restore = () => { ctx.restore(); depth--; };\nfunction assertBalanced() {\n  if ( depth !== 0 ) throw new Error( `unbalanced transforms: depth ${depth}` );\n}","typeGuard":null,"tryCatchPattern":"try {\n  ctx.validateTransformStack();\n} catch ( err ) {\n  if ( /transform stack not 0/.test( err.message ) ) {\n    console.warn( 'leaked save(); draining remaining restores' );\n    try { while ( true ) ctx.restore(); } catch { /* stack drained */ }\n  } else throw err;\n}","preventionTips":["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()."],"tags":["canvas","manual","transform","assertion","save-restore","lesson"],"backgroundTag":null,"analyzedSha":"da05705fa31f02710c19772a2aa70c7454807f0c","analyzedAt":"2026-08-12T22:51:37.160Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}