denoland/deno · error · Error
Destroy hook of " " errored
Error message
Destroy hook of "${id}" errored What it means
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.
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.
Example fix
// before
destroy() {
this.out.close(); // throws if create() never opened it
},
// after
destroy() {
this.out?.close();
}, Defensive patterns
Strategy: try-catch
Try / catch
try {
await runLint(files); // destroy hooks run at teardown
} catch (err) {
if (err.message.startsWith('Destroy hook of')) {
console.error("cleanup failed:", err.cause);
} else throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Visitor " " of plugin " " errored
- Empty node
- Empty selector
- Expected 'of' keyword in ':nth-child' but got
- Expected token ' ', but got ' '.\n\n \n ^
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/13fec3ea501db958.
Report an issue: GitHub.
Appendix: source
Thrown at cli/js/40_lint.js:1305
});
}
};
if (isExit) {
info.exit = wrapped;
} else {
info.enter = wrapped;
}
}
}
if (typeof rule.destroy === "function") {
const destroyFn = rule.destroy.bind(rule);
destroyFns.push(() => {
try {
destroyFn(ruleCtx);
} catch (err) {
throw new Error(`Destroy hook of "${id}" errored`, { cause: err });
}
});
}
}
}
// Create selectors
/** @type {TransformFn} */
const toElem = (str) => {
const id = ctx.typeByStr.get(str);
return id === undefined ? 0 : id;
};
/** @type {TransformFn} */
const toAttr = (str) => {
const id = ctx.propByStr.get(str);
return id === undefined ? 0 : id;
};
View on GitHub (pinned to 9ad36f7a2c)