denoland/deno · error · TypeError

Illegal invocation

Error message

Illegal invocation

What it means

setTimeout/setInterval are bare global functions guarded by checkThis (ext/web/02_timers.js:41-45): calling them with a this other than null, undefined, or globalThis throws TypeError 'Illegal invocation'. The timers module keeps its id->timer map (activeTimers) at module scope and the web-exposed functions must run as globals. Typical trigger: storing setTimeout as an object property or class field and invoking it as a method, so this binds to the receiver.

Source

Thrown at ext/web/02_timers.js:44

  PromisePrototypeThen,
  ReflectApply,
  SafeMap,
  TypeError,
  indirectEval,
} = primordials;

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");

// Map numeric timer IDs to internal core timer objects so clearTimeout /
// clearInterval / refTimer / unrefTimer can look them up by id.
const activeTimers = new SafeMap();

// WHATWG timer nesting depth tracking.
let timerDepth = 0;

function checkThis(thisArg) {
  if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) {
    throw new TypeError("Illegal invocation");
  }
}

/**
 * Call a callback function after a delay.
 */
function setTimeout(callback, timeout = 0, ...args) {
  checkThis(this);
  if (typeof callback !== "function") {
    const unboundCallback = webidl.converters.DOMString(callback);
    callback = () => indirectEval(unboundCallback);
  }
  const unboundCallback = callback;
  const asyncContext = getAsyncContext();
  const depth = timerDepth;
  let id = 0;
  const wrappedCallback = function () {
    const oldContext = getAsyncContext();

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Call timers as bare globals: setTimeout(fn, 1000) or globalThis.setTimeout(fn, 1000).
  2. When capturing, wrap in an arrow ((...a) => setTimeout(...a)) or bind: const st = setTimeout.bind(globalThis).
  3. Search for .call(/.apply( around setTimeout/setInterval and for assignments like obj.wait = setTimeout.

Example fix

// before
this.wait = setTimeout;
this.wait(flush, 1000); // TypeError: Illegal invocation

// after
this.wait = (fn, ms) => setTimeout(fn, ms);
this.wait(flush, 1000);
Defensive patterns

Strategy: fallback

Try / catch

const safeTimer = (...args) => {
  try {
    return receiver.setTimeout(...args);
  } catch (e) {
    if (e instanceof TypeError && e.message === 'Illegal invocation') {
      return setTimeout(...args);
    }
    throw e;
  }
};

Prevention

When it happens

Trigger: setTimeout.call({}, fn, 1000); const t = { set: setTimeout }; t.set(fn, 1000); this.timer = setTimeout; ... this.timer(fn, ms) inside a class — receiver is the instance, not globalThis.

Common situations: Classes capturing timers as fields for testability; DI/instrumentation wrappers that invoke captured functions with .call(receiver); bundlers or mocks re-hosting globals as object members. Node's timers tolerate arbitrary receivers, so code ported from Node can trip Deno's stricter web-shaped guard.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/b038f1ef8ca2b98e. Report an issue: GitHub.