{"record":{"id":"141977bb42b89aef","repo":"facebook/flow","slug":"attempted-to-mutate-a-node-type-key-on-a-d","errorCode":null,"errorMessage":"Attempted to mutate a `${node.type}.${key}` on a deleted node.","messagePattern":"Attempted to mutate a `(.+?)\\.(.+?)` on a deleted node\\.","errorType":"exception","errorClass":"NodeIsDeletedError","httpStatus":null,"severity":"error","filePath":"packages/flow-transform/src/transform/MutationContext.js","lineNumber":65,"sourceCode":"      `Attempted to mutate a \\`${node.type}.${key}\\` when it has already been mutated.`,\n    );\n\n    const map = Array.isArray(\n      // $FlowExpectedError[prop-missing]\n      node[key],\n    )\n      ? this._mutatedArrays\n      : this._mutatedKeys;\n\n    map.set(node, map.get(node)?.add(key) ?? new Set([key]));\n  }\n\n  /**\n   * Throws if the node has been deleted\n   */\n  assertNotDeleted(node: ESNode, message: string): void {\n    if (this._deletedNodes.has(node)) {\n      throw new NodeIsDeletedError(message);\n    }\n  }\n\n  /**\n   * Throws if the key of the node has been mutated\n   */\n  assertNotMutated(node: ESNode, key: string, message: string): void {\n    if (this._mutatedKeys.get(node)?.has(key) === true) {\n      throw new NodeIsMutatedError(message);\n    }\n  }\n\n  appendCommentToSource(comment: Comment, placement: CommentPlacement): void {\n    this.code = appendCommentToSource(this.code, comment, placement);\n  }\n}\n","sourceCodeStart":47,"sourceCodeEnd":82,"githubUrl":"https://github.com/facebook/flow/blob/d1341dac899a79c027762f6b423d896045287620/packages/flow-transform/src/transform/MutationContext.js#L47-L82","documentation":"MutationContext keeps a Set of every node marked deleted (via markDeletion, called by remove and replace mutations, which delete the whole subtree). markMutation runs assertNotDeleted first, so attempting any further mutation on a deleted node throws NodeIsDeletedError with this message. It exists because mutating a detached subtree produces output that silently disappears from the printed code.","triggerScenarios":"Within one transform pass: a visitor first replaces a statement (ReplaceStatementWithMany marks the original deleted) and then another mutation anchors on that same statement or tries to write one of its keys; queueing several mutations against the same target and flushing them together.","commonSituations":"Codemods that both rewrite a statement and separately mutate its children; visitor libraries that collect mutations during traversal and apply them in a batch, letting two operations touch one node.","solutions":["Issue exactly one mutation per node per transform pass","When replacing a statement, fold the child-level changes into the replacement nodes instead of mutating the original afterwards","Keep your own Set of nodes you have removed or replaced and skip them when applying later mutations"],"exampleFix":"// before\nmutations.push(replaceWithMany(stmt, [a, b]));\nmutations.push(mutate(stmt.body, 'key', v)); // stmt already deleted -> throws\n\n// after\nmutations.push(replaceWithMany(stmt, [a, b])); // single mutation owns the node","handlingStrategy":"validation","validationCode":"// keep your own ledger of nodes you have removed or replaced this pass\nconst deleted = new Set();\nfunction safePush(mutation) {\n  if (deleted.has(mutation.target)) return; // skip stale mutations\n  deleted.add(mutation.target);\n  mutations.push(mutation);\n}","typeGuard":"const isNodeDeleted = (mutationContext, node) => mutationContext._deletedNodes.has(node); // private field; prefer your own ledger","tryCatchPattern":"try {\n  mutationContext.markMutation(node, key);\n} catch (e) {\n  if (e.constructor.name === 'NodeIsDeletedError') return; // skip mutations on removed subtrees\n  throw e;\n}","preventionTips":["Apply at most one mutation per node per transform pass","Fold child-level edits into the replacement nodes instead of mutating afterwards","Track removed and replaced nodes in a Set and skip queued mutations that target them"],"tags":["flow-transform","mutation","lifecycle","deleted-node"],"backgroundTag":"mutate-after-delete","analyzedSha":"d1341dac899a79c027762f6b423d896045287620","analyzedAt":"2026-08-17T00:07:02.212Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}