{"record":{"id":"13fec3ea501db958","repo":"denoland/deno","slug":"destroy-hook-of-id-errored","errorCode":null,"errorMessage":"Destroy hook of \"${id}\" errored","messagePattern":"Destroy hook of \"(.+?)\" errored","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/js/40_lint.js","lineNumber":1305,"sourceCode":"              });\n            }\n          };\n\n          if (isExit) {\n            info.exit = wrapped;\n          } else {\n            info.enter = wrapped;\n          }\n        }\n      }\n\n      if (typeof rule.destroy === \"function\") {\n        const destroyFn = rule.destroy.bind(rule);\n        destroyFns.push(() => {\n          try {\n            destroyFn(ruleCtx);\n          } catch (err) {\n            throw new Error(`Destroy hook of \"${id}\" errored`, { cause: err });\n          }\n        });\n      }\n    }\n  }\n\n  // Create selectors\n  /** @type {TransformFn} */\n  const toElem = (str) => {\n    const id = ctx.typeByStr.get(str);\n    return id === undefined ? 0 : id;\n  };\n  /** @type {TransformFn} */\n  const toAttr = (str) => {\n    const id = ctx.propByStr.get(str);\n    return id === undefined ? 0 : id;\n  };\n","sourceCodeStart":1287,"sourceCodeEnd":1323,"githubUrl":"https://github.com/denoland/deno/blob/89f33cbef296a2b287f323d42de54c871fa69c77/cli/js/40_lint.js#L1287-L1323","documentation":"Thrown when a rule's optional `destroy()` hook raises. During lint setup (cli/js/40_lint.js, runPluginsForFile) destroy hooks are collected and each is wrapped so a failure is reported as 'Destroy hook of \"<plugin>/<rule>\" errored' with the original error as `cause`. Destroy runs after linting a file finishes, for cleanup.","triggerScenarios":"A rule defines `destroy(ctx)` and the function throws: releasing a resource that was never acquired, referencing state that a crashed visitor left inconsistent, or calling a method on undefined cleanup state.","commonSituations":"Rules that accumulate per-file state (maps of imports, reported spans) and clean up in destroy; a destroy hook that assumes create() ran successfully for every file; refactoring a rule and forgetting destroy still reads an old field.","solutions":["Look at err.cause for the real stack inside your destroy() implementation","Make destroy defensive: only clean up what was actually initialized, and tolerate partially-populated state","If the hook has nothing to clean up, remove it entirely"],"exampleFix":"// before\ndestroy(ctx) {\n  this.seen.forEach((n) => n.parent.unregister(this));\n},\n\n// after\ndestroy(ctx) {\n  for (const entry of this.acquired) entry.release();\n},","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await runLint(fixture);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Destroy hook of\")) {\n    // inspect err.cause for the real cleanup failure\n    console.error(\"cleanup bug:\", err.cause ?? err);\n  }\n  throw err;\n}","preventionTips":["Make destroy() idempotent and tolerant of partially-initialized state","Only acquire resources in create() that destroy() actually needs to release"],"tags":["lint","plugin","lifecycle","runtime-error"],"backgroundTag":null,"analyzedSha":"89f33cbef296a2b287f323d42de54c871fa69c77","analyzedAt":"2026-08-16T07:54:21.310Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}