javascript
async loading
DOM manipulation
callback function
web development

Load javascript async, then check DOM loaded before executing callback

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When a script is loaded asynchronously, it may finish before or after the DOM is ready. If the callback touches page elements, the safe pattern is to wait for both conditions: the external script must be loaded, and the document must have reached a usable ready state.

Wait For The Script And The DOM

There are two separate events involved:

  • The script file finishes loading.
  • The document is ready for DOM queries and updates.

Treating those as one event is what causes race conditions. A reliable solution is to model each one explicitly, then run the callback only after both promises resolve.

javascript
1function loadScript(url) {
2  return new Promise((resolve, reject) => {
3    const script = document.createElement("script");
4    script.src = url;
5    script.async = true;
6    script.onload = () => resolve(url);
7    script.onerror = () => reject(new Error(`Failed to load ${url}`));
8    document.head.appendChild(script);
9  });
10}
11
12function domReady() {
13  return new Promise((resolve) => {
14    if (document.readyState === "interactive" || document.readyState === "complete") {
15      resolve();
16      return;
17    }
18
19    document.addEventListener("DOMContentLoaded", resolve, { once: true });
20  });
21}
22
23async function loadAndRun(url, callback) {
24  await Promise.all([loadScript(url), domReady()]);
25  callback();
26}
27
28loadAndRun("/assets/widget.js", () => {
29  const target = document.querySelector("#status");
30  if (target) {
31    target.textContent = "Widget loaded after DOM ready";
32  }
33});

This pattern is robust because it does not assume which event happens first. If the DOM is already ready, domReady() resolves immediately. If the script finishes later, Promise.all still waits for it.

Why async Alone Is Not Enough

The async attribute improves page performance because the browser downloads the script without blocking HTML parsing. The tradeoff is that execution timing becomes independent from DOM parsing order.

That is fine for scripts that do not care about the page structure, but it is risky for code that does something like document.querySelector("#menu"). If that node has not been parsed yet, the callback sees null and fails in a way that looks intermittent.

If you control the markup and just need a static page script, defer is often simpler than async because deferred scripts execute after parsing. But when you are dynamically injecting a script, using a promise-based loader plus a DOM readiness check is the most flexible approach.

Example With A Third-Party SDK

A common real-world use case is loading an SDK only on pages that need it. In that case, the callback should run only after the library exists and the placeholder element has been parsed.

javascript
1async function initializeAnalyticsWidget() {
2  await Promise.all([
3    loadScript("https://example.com/sdk.js"),
4    domReady()
5  ]);
6
7  const mountPoint = document.querySelector("#analytics-widget");
8  if (!mountPoint) {
9    return;
10  }
11
12  window.AnalyticsWidget.init({
13    target: mountPoint,
14    theme: "light"
15  });
16}
17
18initializeAnalyticsWidget().catch((error) => {
19  console.error(error.message);
20});

The important detail is that window.AnalyticsWidget.init is called only after the SDK has loaded. Waiting for DOM readiness alone would not guarantee that the global object exists.

Use The Right Ready Signal

For DOM access, DOMContentLoaded is usually the correct event. It fires after the initial HTML has been fully parsed. The later load event waits for images, stylesheets, and other external resources, which often adds unnecessary delay.

Choose load only when the callback truly depends on those extra assets. For most UI initialization, DOMContentLoaded gives the earliest safe point.

Also note that if your code executes after the document is already ready, adding a listener is not enough by itself. That is why the helper checks document.readyState first. Without that check, late calls can stall forever because the event has already passed.

Common Pitfalls

The first pitfall is assuming script.onload means the DOM is ready. It only means the script file finished loading and executing.

Another pitfall is using window.onload for everything. It works, but it delays execution until far later than necessary, which can make widgets feel sluggish.

Some code accidentally calls the callback twice: once from DOMContentLoaded and once from onload. Wrapping both prerequisites in promises avoids duplicate execution.

Error handling is also often missing. If the network request for the script fails, a silent failure can leave the page in a broken state. Rejecting the promise and logging or recovering from the error makes the behavior easier to debug.

Finally, be careful with dependencies between async scripts. If one script expects another global to exist, load them in explicit sequence rather than starting them all at once.

Summary

  • Async script loading and DOM readiness are separate conditions.
  • A reliable solution waits for both with Promise.all.
  • 'DOMContentLoaded is usually the right signal for DOM-safe initialization.'
  • Check document.readyState so late callers still resolve correctly.
  • Add error handling for failed script loads and avoid duplicate callback paths.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.