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

  1. Inspect `error.cause` (not just the message) — run deno lint and print the cause chain to get the original stack from your plugin.
  2. Guard optional AST fields in the visitor (check for null/undefined before reading .type or indices).
  3. Reproduce with the smallest file that triggers it, then fix the plugin code.
  4. 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

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


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)