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

  1. Read `error.cause` to get the original exception and stack from your destroy hook.
  2. Make the destroy hook tolerant of empty/no-op state: use optional chaining and check collections before flushing.
  3. Move logic that can legitimately throw out of destroy and into the visitor, keeping destroy strictly for cleanup.
  4. 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

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


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)