denoland/deno · error · TypeError

ERR_ILLEGAL_CONSTRUCTOR

ERR_ILLEGAL_CONSTRUCTOR

Error message

Illegal constructor

What it means

The Scheduler class exported by node:timers implements the Scheduling API, but there is exactly one scheduler per realm: Node and this Deno polyfill both make `new Scheduler()` throw ERR_ILLEGAL_CONSTRUCTOR. The class is exported only for identity checks such as `scheduler instanceof Scheduler`. All functionality lives on the exported `scheduler` singleton, whose methods also reject foreign `this` values with ERR_INVALID_THIS.

Source

Thrown at ext/node/polyfills/timers.ts:371

const promises = {
  setTimeout: setTimeoutPromise,
  setImmediate: setImmediatePromise,
  setInterval: setIntervalAsync,
};

// There is exactly one `scheduler`, so identity is the check: a
// prototype-linked object, or one carrying whatever properties the real
// instance has, is still a foreign receiver.
function validateScheduler(self: unknown) {
  if (self !== scheduler) {
    throw new ERR_INVALID_THIS("Scheduler");
  }
}

class Scheduler {
  constructor() {
    throw new ERR_ILLEGAL_CONSTRUCTOR();
  }
  // Not `async`: the `this` check has to reject a foreign receiver
  // synchronously rather than returning a rejected promise.
  wait(
    delay: number,
    options?: { signal?: AbortSignal },
  ): Promise<void> {
    validateScheduler(this);
    return setTimeoutPromise(delay, undefined, options);
  }
  yield() {
    validateScheduler(this);
    return promises.setImmediate();
  }
}

const scheduler = ObjectCreate(Scheduler.prototype);
promises.scheduler = scheduler;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Use the exported singleton: `import { scheduler } from 'node:timers'` and call `scheduler.wait(ms)` or `scheduler.yield()`
  2. For dependency injection or tests, wrap the singleton in your own function instead of subclassing Scheduler
  3. Keep importing Scheduler only for `instanceof` assertions
  4. Use timers/promises `setTimeout(ms)` when you only need a delayable promise

Example fix

// before
import { Scheduler } from "node:timers";
const s = new Scheduler(); // ERR_ILLEGAL_CONSTRUCTOR
await s.wait(100);

// after
import { scheduler } from "node:timers";
await scheduler.wait(100);
Defensive patterns

Strategy: type-guard

Type guard

import { scheduler } from "node:timers";
const isScheduler = (v: unknown): v is typeof scheduler => v === scheduler;

Try / catch

try {
  await runLibrary(opts); // may construct Scheduler internally
} catch (e: any) {
  if (e?.code === "ERR_ILLEGAL_CONSTRUCTOR") {
    await scheduler.wait(opts.delay); // use the singleton instead
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Executing `new Scheduler()` after importing it from node:timers; instantiating a subclass (`class S extends Scheduler {}` then `new S()`); calling `Scheduler.prototype.wait.call({}, 100)`; test utilities that enumerate module exports and try to construct every class.

Common situations: Porting scheduling code written against a constructible API; attempting per-request or per-test scheduler instances for isolation; mocking node:timers by subclassing Scheduler.

Related errors


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