denoland/deno · error · Error
Visitor " " of plugin " " errored
Error message
Visitor "${name}" of plugin "${id}" errored What it means
When a lint plugin rule's visitor callback (an enter handler or a ":exit" handler) throws while visiting a node, the linter wraps the exception as `Visitor "<ruleName>" of plugin "<pluginName>/<ruleName>" errored` (cli/js/40_lint.js). The original exception is preserved on `error.cause`. This identifies which rule failed mid-file, since a raw stack from plugin code alone would not say which selector key triggered it.
Solutions
- Inspect `error.cause` (not just the message) — run deno lint and print the cause chain to get the original stack from your plugin.
- Guard optional AST fields in the visitor (check for null/undefined before reading .type or indices).
- Reproduce with the smallest file that triggers it, then fix the plugin code.
- Lint the failing file with the plugin disabled to confirm the plugin (not the file) is the problem.
Example fix
// before
"VariableDeclaration:exit"(node) {
if (node.declarations[0].init.type === "Literal") {
ctx.report({ node, message: "no literal init" });
}
},
// after
"VariableDeclaration:exit"(node) {
const init = node.declarations[0]?.init;
if (init !== null && init !== undefined && init.type === "Literal") {
ctx.report({ node, message: "no literal init" });
}
}, Defensive patterns
Strategy: try-catch
Validate before calling
// Fuzz the plugin over a fixture corpus during development so visitor bugs // surface in CI instead of during a real lint run: // deno lint --plugin=./my-plugin.js ./fixtures/
Try / catch
// In tooling that drives the linter programmatically:
try {
runLint(files);
} catch (err) {
const root = err.cause ?? err; // original plugin exception
console.error(`lint plugin failure: ${err.message}`);
console.error(root.stack ?? root);
Deno.exit(1);
} Prevention
- Treat every AST field as optional: use ?. and explicit null checks before reading .type or indexing arrays.
- Run the plugin against a fixture corpus (unusual syntax, empty programs, edge-case nodes) in CI.
- When this error fires, always inspect error.cause — the wrapper message alone has no stack from your code.
When it happens
Trigger: A rule's create(ctx) returns a visitor like `{ "VariableDeclaration:exit": (node) => ... }` and the callback throws on some node during `deno lint` — e.g. reading node.declarations[0].init.type when init is null, passing bad arguments to ctx.report, or any bug in the plugin's logic for an edge-case AST shape.
Common situations: Visitor assumes optional AST fields always exist (init, directive, typeParameters, elements containing null holes); AST shape differences after a Deno upgrade; plugin logic that only fails on unusual syntax (decorators, optional chaining, top-level await).
Related errors
- Destroy hook of " " 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/bff530b590f77598.
Report an issue: GitHub.
Appendix: source
Thrown at cli/js/40_lint.js:1285
const key = selectors[j];
let info = bySelector.get(key);
if (info === undefined) {
info = { enter: NOOP, exit: NOOP };
bySelector.set(key, info);
}
const prevFn = isExit ? info.exit : info.enter;
/**
* @param {*} node
*/
const wrapped = (node) => {
prevFn(node);
try {
fn(node);
} catch (err) {
throw new Error(`Visitor "${name}" of plugin "${id}" errored`, {
cause: err,
});
}
};
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);View on GitHub (pinned to 9ad36f7a2c)