{"record":{"id":"1afd5c5b5b6c2dad","repo":"emberjs/ember.js","slug":"replacing-and-removing-in-key-handlers-is-not-yet","errorCode":null,"errorMessage":"Replacing and removing in key handlers is not yet supported.","messagePattern":"Replacing and removing in key handlers is not yet supported\\.","errorType":"exception","errorClass":"TraversalError","httpStatus":null,"severity":"error","filePath":"packages/@glimmer/syntax/lib/traversal/traverse.ts","lineNumber":182,"sourceCode":"  let value = get(node, key);\n  if (!value) {\n    return;\n  }\n\n  let keyEnter: KeyHandler<N, VisitorKeys[N['type']] & keyof N> | undefined;\n  let keyExit: KeyHandler<N, VisitorKeys[N['type']] & keyof N> | undefined;\n\n  if (handler !== undefined) {\n    let keyHandler = getKeyHandler(handler, key);\n    if (keyHandler !== undefined) {\n      keyEnter = getEnterFunction(keyHandler);\n      keyExit = getExitFunction(keyHandler);\n    }\n  }\n\n  if (keyEnter !== undefined) {\n    if (keyEnter(node, key) !== undefined) {\n      throw cannotReplaceOrRemoveInKeyHandlerYet(node, key);\n    }\n  }\n\n  if (Array.isArray(value)) {\n    visitArray(visitor, value, path, key);\n  } else {\n    let keyPath = new WalkerPath(value, path, key);\n    let result = visitNode(visitor, keyPath);\n    if (result !== undefined) {\n      // TODO: dynamically check the results by having a table of\n      // expected node types in value space, not just type space\n\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      assignKey(node, key, value, result as any);\n    }\n  }\n\n  if (keyExit !== undefined) {","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/emberjs/ember.js/blob/26f97246a8bf2e28edf26ac3093da2e86c04ffc5/packages/@glimmer/syntax/lib/traversal/traverse.ts#L164-L200","documentation":"During traversal, a visitor's key 'enter' handler returned a value, meaning it tried to replace or remove the node. Traversal keyed handlers (per-property) don't support replacement/removal yet, so visitKey throws. This is a visitor API limitation, not template syntax.","triggerScenarios":"Writing a NodeVisitor whose handler for a specific key (via #visit in the traversal API, e.g. visiting Key ASTv1 nodes keyed by property) returns a non-undefined value from the key enter callback.","commonSituations":"Writing codemods/transformers over Glimmer ASTs; assuming visitor return values replace nodes like in other AST tools (e.g. babel); porting node-level visitors to key-level handlers.","solutions":["Return undefined from the key handler; perform replacement in the node-level handler instead","Use the node handler (per node type) where replacement/removal IS supported","Mutate the node in place rather than returning a replacement"],"exampleFix":"// before\nvisit(node, { keys: { parts: (node, key) => [] } }) // returns value -> throws\n// after\nvisit(node, { Block: (node) => ({ ...node, parts: [] }) }) // replace via node handler","handlingStrategy":"type-guard","validationCode":"// assert key handlers never return a value\nfunction safeKeyHandler(fn) { return (node, key) => { const r = fn(node, key); if (r !== undefined) throw new Error('key handlers cannot replace/remove: return undefined'); }; }","typeGuard":"const returnsNothing = (fn) => fn.length >= 0 && undefined; // wrap: (node,key)=>{ fn(node,key); return undefined; }","tryCatchPattern":"try { visit(ast, visitor) } catch (e) { if (/Replacing and removing in key handlers/.test(e.message)) { /* fall back to node-level handler transform */ } else throw e; }","preventionTips":["Key handlers must return undefined; only node handlers may replace/remove","Write a unit test for your visitor that traverses a sample AST","Mutate nodes in place instead of returning replacements in key handlers"],"tags":["glimmer","ast","traversal","visitor-api"],"backgroundTag":"visitor-replace-not-supported","analyzedSha":"26f97246a8bf2e28edf26ac3093da2e86c04ffc5","analyzedAt":"2026-09-01T05:01:28.182Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}