denoland/deno · error · TypeError

resolve hook must return { shortCircuit: true } or call next

Error message

resolve hook must return { shortCircuit: true } or call nextResolve

What it means

Under Node's module.registerHooks() (synchronous module customization hooks, polyfilled by Deno's CJS loader in ext/node/polyfills/01_require.js), every resolve hook must either delegate by calling nextResolve() or take responsibility by returning an object containing shortCircuit: true. If the hook returns undefined (or anything lacking shortCircuit) and never chained, the runner throws this TypeError because the resolution chain cannot continue.

Source

Thrown at ext/node/polyfills/01_require.js:757

        try {
          const resolved = new URL(spec, currentContext.parentURL).href;
          return { url: resolved, shortCircuit: true };
        } catch {
          return { url: null, shortCircuit: true };
        }
      } finally {
        insideResolveHook = false;
      }
    }
    const hook = resolveHooks[index++];
    let nextCalled = false;
    const wrappedNext = (s, c) => {
      nextCalled = true;
      return nextResolve(s, c);
    };
    const result = hook(spec, currentContext, wrappedNext);
    if (!nextCalled && !result?.shortCircuit) {
      throw new TypeError(
        "resolve hook must return { shortCircuit: true } or call nextResolve",
      );
    }
    return result;
  }

  return nextResolve(specifier, context);
}

function esmResolveHookCallback(specifier, referrer, importAttributes) {
  const attrs = { __proto__: null };
  if (importAttributes !== null && typeof importAttributes === "object") {
    for (const key in importAttributes) {
      attrs[key] = importAttributes[key];
    }
  }
  const context = {
    parentURL: referrer || undefined,

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Make every code path either return { url, shortCircuit: true, format } or return nextResolve(spec, context)
  2. Use the ternary shape: return cond ? { url, shortCircuit: true } : nextResolve(spec, context)
  3. Add an explicit `return nextResolve(spec, context);` as the last statement of the hook body

Example fix

// before
module.registerHooks({
  resolve(spec, context, nextResolve) {
    if (spec.startsWith("virtual:")) {
      return { url: `virtual:${spec}`, shortCircuit: true, format: "module" };
    }
    // falls through: no return, no nextResolve -> TypeError
  },
});

// after
module.registerHooks({
  resolve(spec, context, nextResolve) {
    if (spec.startsWith("virtual:")) {
      return { url: `virtual:${spec}`, shortCircuit: true, format: "module" };
    }
    return nextResolve(spec, context);
  },
});
Defensive patterns

Strategy: validation

Validate before calling

// Write hooks as total functions: every path either short-circuits or chains.
const resolveHook = (spec: string, context: object, nextResolve: Function) =>
  spec.startsWith("virtual:")
    ? { url: `virtual:${spec}`, shortCircuit: true, format: "module" }
    : nextResolve(spec, context);

module.registerHooks({ resolve: resolveHook });

Type guard

function isResolveResult(v: unknown): v is { url: string; shortCircuit?: boolean; format?: string } {
  return v != null && typeof v === "object" && "url" in v;
}

Prevention

When it happens

Trigger: module.registerHooks({ resolve(spec, ctx, nextResolve) { if (spec.startsWith('virtual:')) return { url: spec, shortCircuit: true }; } }) - the non-virtual path neither returns a shortCircuit result nor calls nextResolve; forgetting `return` in an arrow body; a conditional that only returns on some branches.

Common situations: Porting registerHooks or module.register loader code from Node; writing hooks where the default path silently falls off the end; refactoring hooks and dropping the final return nextResolve(...) line.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/7a2876aedafb57d4. Report an issue: GitHub.