Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use CustomElementRegistry.whenDefined() when your Node.js code is running with a DOM-capable environment:

await customElements.whenDefined('my-widget');

The promise resolves when the registry has a constructor for my-widget, and resolves immediately if the element was already registered. A bare Node.js process does not normally provide a browser DOM or customElements; your browser automation tool, test runner or DOM implementation must expose that registry first.

What “wait for a custom element” actually means

There are three different conditions that are often confused:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Definition: the custom-element registry has a constructor for a name.
  • Connection: an instance has been attached to the document.
  • Readiness: that instance has completed application-specific asynchronous work such as data loading.

whenDefined() handles only the first condition. It is event-based, so it avoids guessing a delay.

Wait for one element with whenDefined()

ES modules

await customElements.whenDefined('my-widget');

const Widget = await customElements.whenDefined('my-widget');
const element = document.querySelector('my-widget');
if (element) {
  // The class is registered; the instance may still need its own ready signal.
  console.log(Widget.name);
}

The returned promise fulfills with the element constructor. If another script has already called customElements.define('my-widget', Widget), the await completes without waiting.

CommonJS

async function main() {
  const Widget = await globalThis.customElements.whenDefined('my-widget');
  console.log('registered:', Widget.name);
}

main().catch(console.error);

CommonJS syntax does not change the registry API. The important prerequisite is still a DOM-capable global.

Wait for several custom elements

Deduplicate names and await them together. Promise.all() rejects as soon as one wait rejects.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const names = new Set([
  'my-widget',
  'site-header',
  'my-widget' // duplicate is removed
]);

await Promise.all(
  [...names].map((name) => customElements.whenDefined(name))
);

console.log('all requested elements are registered');

This waits for registration, not for instances to render. If the application can legitimately omit one of the names, wait for only the names that are expected in that test or page.

Make sure the name is valid

Custom-element names have naming rules, including a lowercase initial character and a hyphen. Names such as my-widget are valid; MyWidget is not a valid custom-element name. An invalid name causes whenDefined() to reject with a syntax error rather than waiting forever.

try {
  await customElements.whenDefined('MyWidget');
} catch (error) {
  console.error('Invalid custom-element name:', error);
}

A valid name can still remain pending indefinitely if the module that defines it never loads, takes a different code path, or throws before calling customElements.define().

Node.js does not automatically include a custom-element registry

Node.js is a JavaScript runtime, not a browser window. In a plain process, this may be absent:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
console.log(typeof globalThis.customElements); // commonly "undefined" in bare Node.js

Availability depends on the environment hosting your code: a browser automation context, a DOM implementation, or a test runner that implements custom elements. Check that environment’s documentation for its support and lifecycle behavior.

Fail clearly when no registry exists

function getRegistry() {
  const registry = globalThis.customElements;
  if (!registry || typeof registry.whenDefined !== 'function') {
    throw new Error(
      'No CustomElementRegistry is available. Run this code in a DOM-capable context.'
    );
  }
  return registry;
}

await getRegistry().whenDefined('my-widget');

This diagnostic is preferable to a cryptic “cannot read properties of undefined” failure.

When a timer is appropriate—and when it is not

Node’s node:timers/promises module can wait for elapsed time, but it cannot observe a custom-element registration.

import { setTimeout as delay } from 'node:timers/promises';

await delay(250); // time passed; registration is not guaranteed

In CommonJS:

const { setTimeout: delay } = require('node:timers/promises');
await delay(250);

Use a timer only when the requirement genuinely is “pause for approximately this duration.” Node’s timers documentation warns that callback timing and ordering are not exact guarantees. A timer can finish before a slow module registers the element, or waste time after registration has already happened.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Cancel a delay with an AbortSignal

import { setTimeout as delay } from 'node:timers/promises';

const controller = new AbortController();
const timer = delay(5000, undefined, { signal: controller.signal });

// Cancel from another event, timeout policy, or test teardown:
controller.abort();

try {
  await timer;
} catch (error) {
  console.error('Delay cancelled:', error.name);
}

Cancellation applies to the delay. It does not cancel or replace whenDefined().

Wait for an instance to be ready

If your real requirement is “the widget is usable,” add an explicit readiness contract. For example, a component can dispatch a one-time event after its asynchronous setup:

class MyWidget extends HTMLElement {
  async connectedCallback() {
    await this.loadData();
    this.dispatchEvent(new Event('ready'));
  }

  async loadData() {
    // application-specific work
  }
}
customElements.define('my-widget', MyWidget);

await customElements.whenDefined('my-widget');
const widget = document.querySelector('my-widget');
if (!widget) throw new Error('Expected my-widget instance was not found');
await new Promise((resolve) => widget.addEventListener('ready', resolve, { once: true }));

Alternatively, expose a component-owned promise such as widget.ready. The exact signal belongs to the component; registration alone cannot promise that data, painting, or event listeners are complete.

Reliable patterns for tests and browser automation

Import the definition before waiting

await import('./my-widget.js');
await customElements.whenDefined('my-widget');

This is useful when the module is expected to perform registration as a side effect. If the import throws, fix that error first; the wait cannot succeed without a successful definition.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait before querying or interacting

await customElements.whenDefined('site-header');
const header = document.querySelector('site-header');
if (!header) throw new Error('site-header is not present in the document');
header.click();

Waiting before the query avoids racing registration, while the explicit null check catches a separate markup problem.

Use a bounded wait policy

whenDefined() itself has no timeout. For a test suite, race it against a timeout so a missing definition produces a controlled failure:

function withTimeout(promise, milliseconds, label) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`Timed out waiting for ${label}`)), milliseconds)
    )
  ]);
}

await withTimeout(
  customElements.whenDefined('my-widget'),
  5000,
  'my-widget definition'
);

The timeout is a failure guard, not a substitute for the registration event.

Troubleshooting

“customElements is not defined”

Cause: code is running in bare Node.js or in a context that did not install a DOM global.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix: execute the wait inside the browser page or DOM-capable test context, or configure the chosen DOM implementation according to its documentation. Do not assume that importing a Node module creates a browser registry.

The promise never settles

Cause: the defining module did not load, threw during evaluation, used a different name, or never reached customElements.define().

Fix: await the module import, inspect its error, verify the exact lowercase-and-hyphenated name, and add a bounded timeout in tests.

A syntax error is thrown immediately

Cause: the supplied name violates custom-element naming rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix: use a valid name such as order-card, and use the identical string in the HTML, definition, and wait.

The wait completes but the element is still not usable

Cause: registration finished, but the instance is not connected or its asynchronous setup is incomplete.

Fix: query the instance, verify it exists, then await the component’s documented ready event or promise.

Tests pass locally but fail in CI

Cause: different DOM or browser-runtime versions, module timing, or an omitted page-context step.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix: log the runtime’s registry availability, await definition modules explicitly, and keep registration waits separate from instance-readiness assertions.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability and cost considerations

whenDefined() is preferable to polling because it waits on the registry’s actual event and resolves immediately for an existing definition. Waiting for several names with one Promise.all() also avoids serial delays. Keep a timeout around external test setup so a broken import cannot hang a worker indefinitely. Timers should be reserved for deliberate delays, not synchronization.

There is no Node.js charge for these language and platform APIs. Any cost, startup overhead, or compatibility limit comes from the browser automation or DOM environment you choose to host them.

Or skip the browser setup

If your goal is to obtain a clean screenshot or PDF rather than test a component’s lifecycle, ScreenshotNeo makes one request to capture a URL. It accepts cookie and consent banners as a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and lets you turn each cleanup step off. Only clean shots are billed; bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are free, with the result identified by X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for all options. The same call in Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free.

FAQ

Does whenDefined() wait for an element to appear in the DOM?

No. It waits for registration of the name. Query the document separately and check that an instance exists.

Can I use setTimeout instead?

Only for a fixed delay. It cannot detect registration and can be early or unnecessarily late.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What does the promise return?

It fulfills with the registered custom-element constructor.

Can I wait for a name that is never defined?

The promise can remain pending indefinitely, so use a timeout policy in tests or other bounded workflows.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.