{"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/9ad36f7a2cce60488e6ec52283efb32efddaf93a/cli/js/40_lint.js#L1287-L1323","documentation":"A lint plugin rule may define a destroy(ctx) hook that the linter calls after linting finishes (collected into destroyFns in cli/js/40_lint.js). If that hook throws, the error is wrapped as `Destroy hook of \"<pluginName>/<ruleName>\" errored` with the original exception on `error.cause`. Because destroy hooks run at teardown, this typically surfaces after files were already linted.","triggerScenarios":"A rule with `destroy(ctx)` whose body throws during cleanup: closing a resource that was never opened (because create found nothing to track), reading a Map/accumulator in an invalid state, or referencing a field that was only initialized when the visitor ran.","commonSituations":"Destroy hooks that flush collected diagnostics and fail when the collection is empty; plugins that open files or timers in create() and assume the visitor always ran at least once; cleanup logic written for the happy path only.","solutions":["Read `error.cause` to get the original exception and stack from your destroy hook.","Make the destroy hook tolerant of empty/no-op state: use optional chaining and check collections before flushing.","Move logic that can legitimately throw out of destroy and into the visitor, keeping destroy strictly for cleanup.","Test the plugin against an empty file so destroy runs with zero collected state."],"exampleFix":"// before\ndestroy() {\n  this.out.close(); // throws if create() never opened it\n},\n\n// after\ndestroy() {\n  this.out?.close();\n},","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await runLint(files); // destroy hooks run at teardown\n} catch (err) {\n  if (err.message.startsWith('Destroy hook of')) {\n    console.error(\"cleanup failed:\", err.cause);\n  } else throw err;\n}","preventionTips":["Keep destroy() strictly for cleanup; never put logic there that can legitimately throw.","Assume create/visitor may never have run: use optional chaining (this.out?.close()) and check collections before flushing.","Test the plugin against an empty file so destroy executes with zero collected state."],"tags":["deno","deno-lint","lint-plugin","destroy-hook","lifecycle","error-cause"],"backgroundTag":"plugin-callback-threw","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}