Question

How to Run JavaScript After Page Load in JavaScript

htmljavascriptdomdom-eventspageload

Question

I am loading an external JavaScript file from a <script> tag inside the <head>.

Because that script runs before the page has fully loaded, I cannot reliably access elements inside the <body> and other parts of the document yet.

How can I run JavaScript only after the document has finished loading into memory? Are there browser events I can listen to so code runs when the page is ready?

Short Answer

By the end of this page, you will understand how browsers load HTML and JavaScript, why code in the <head> may run too early, and how to delay execution until the document is ready. You will learn when to use DOMContentLoaded, when to use load, and when simply moving a <script> tag is enough.

Concept

JavaScript can run before the browser has finished building the page.

When the browser reads HTML from top to bottom, it parses elements as it encounters them. If it finds a normal <script> tag, it usually pauses HTML parsing, downloads the script if needed, and executes it immediately. That means code in a script inside <head> may run before <body> elements exist in the DOM.

This matters because many scripts need to do things like:

  • find elements with document.getElementById(...)
  • attach event listeners to buttons or forms
  • change text or styles in the page
  • initialize UI components

If those elements have not been parsed yet, your code may fail or return null.

There are several ways to solve this:

  • listen for the DOMContentLoaded event
  • listen for the load event
  • place the script at the end of <body>
  • use the defer attribute on the script

DOMContentLoaded

This event fires when the HTML document has been fully parsed and the DOM is ready. It does not wait for images, stylesheets in every case of rendering completion, or other external resources to fully finish loading.

This is the most common choice when your code only needs the document structure.

load

This event fires later, after the whole page and its dependent resources have finished loading, including images and other assets.

Use this only when your code depends on those resources being loaded too.

Why this concept matters

In real applications, timing issues are common. If code runs too early, you can get:

  • null element references
  • event listeners attached to nothing
  • broken UI initialization
  • inconsistent bugs depending on network speed

Understanding page load timing helps you write code that runs reliably in browsers.

Mental Model

Think of the browser like a person assembling furniture from instructions.

  • The HTML is the instruction manual being read from top to bottom.
  • The DOM is the assembled furniture.
  • A script in <head> is like interrupting the person halfway through and asking them to use a drawer that has not been assembled yet.

DOMContentLoaded means: the furniture is assembled enough to use.

load means: the furniture is assembled and every extra item in the room has also arrived.

If you only need the page structure, wait for DOMContentLoaded. If you need everything, including images and external resources, wait for load.

Take Quiz

Syntax and Examples

Basic syntax with DOMContentLoaded

document.addEventListener('DOMContentLoaded', function () {
  const message = document.getElementById('message');
  message.textContent = 'The DOM is ready!';
});

This waits until the browser has parsed the HTML and built the DOM. After that, #message can be accessed safely.

Using load

window.addEventListener('load', function () {
  console.log('Entire page and resources are loaded');
});

Use this when your code depends on things like image dimensions or fully loaded external assets.

Modern shorthand with arrow function

document.addEventListener('DOMContentLoaded', () => {
  const button = document.querySelector('button');

  if (button) {
    button.addEventListener('click', () => {
      console.log('Button clicked');
    });
  }
});

Alternative: place the script at the end of <body>

<body>
  <h1 id="title">Hello</h1>

  <script src="app.js"></script>
</body>

When the script tag is placed just before </body>, most DOM elements have already been parsed by the time the script runs.

Alternative: use defer

<head>
  <script src="app.js" defer></script>
</head>

A deferred script downloads during parsing but runs after the HTML has been parsed. This is often the best option for external scripts in <head>.

Step by Step Execution

Consider this HTML:

<!DOCTYPE html>
<html>
<head>
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      const box = document.getElementById('box');
      box.textContent = 'Ready';
    });
  </script>
</head>
<body>
  <div id="box">Loading...</div>
</body>
</html>

Here is what happens step by step:

  1. The browser starts parsing the HTML.
  2. It reaches the <script> inside <head>.
  3. The script runs immediately.
  4. That script does not try to access #box yet.
  5. Instead, it registers a listener for DOMContentLoaded.
  6. The browser continues parsing the rest of the page.
  7. It reaches <body> and creates the <div id="box"> element in the DOM.
  8. Once the HTML is fully parsed, the browser fires DOMContentLoaded.
  9. The callback runs.
  10. document.getElementById('box') now returns the real element.
  11. The text changes from Loading... to Ready.

What would break without waiting?

<script>
  const box = document.getElementById('box');
  box.textContent = 'Ready';
</script>

If this code runs in <head>, box will likely be null because the browser has not parsed the <body> yet.

That can cause an error like:

Cannot set properties of null

Real World Use Cases

This concept appears in many real applications:

Initializing UI components

A script may need to find menus, modals, tabs, or forms before attaching behavior.

document.addEventListener('DOMContentLoaded', () => {
  setupMenu();
  setupTabs();
});

Attaching form validation

Form elements must exist before event listeners can be attached.

document.addEventListener('DOMContentLoaded', () => {
  const form = document.querySelector('#signup-form');
  if (form) {
    form.addEventListener('submit', validateForm);
  }
});

Reading image size after full page load

If you need actual image dimensions, load can be more appropriate.

window.addEventListener('load', () => {
  const img = document.querySelector('#hero');
  console.log(img.naturalWidth, img.naturalHeight);
});

Analytics or third-party widgets

Some scripts wait until the document is ready so they can safely inject UI or collect page information.

Progressive enhancement

You first render a usable HTML page, then use JavaScript after load to improve interaction.

Take Quiz

Real Codebase Usage

In real codebases, developers usually do more than just attach one listener.

Common patterns

1. Initialization function

function init() {
  const nav = document.querySelector('.nav');
  const form = document.querySelector('#contact-form');

  if (nav) {
    setupNav(nav);
  }

  if (form) {
    setupForm(form);
  }
}

document.addEventListener('DOMContentLoaded', init);

This keeps startup logic organized.

2. Guard clauses

Developers often check whether an element exists before using it.

document.addEventListener('DOMContentLoaded', () => {
  const modal = document.querySelector('.modal');
  if (!modal) return;

  modal.classList.add('ready');
});

This avoids runtime errors on pages where some elements are missing.

3. Using defer for external scripts

In many projects, JavaScript files are loaded like this:

<script src="main.js" defer></script>

Then main.js can often access the DOM directly because execution happens after parsing.

4. Event delegation after DOM ready

Apps often attach one listener to a parent container after the DOM is available.

document.addEventListener('DOMContentLoaded', () => {
  document.body.addEventListener('click', (event) => {
    if (event.target.matches('.delete-button')) {
      console.log('Delete clicked');
    }
  });
});

5. Separate setup from business logic

A common structure is:

  • wait for document readiness
  • find elements
  • attach listeners
  • call reusable functions

This makes code easier to test and maintain.

Common Mistakes

1. Accessing elements too early

Broken code:

<head>
  <script>
    const el = document.getElementById('app');
    el.textContent = 'Hello';
  </script>
</head>

Problem:

  • #app may not exist yet.

Fix:

document.addEventListener('DOMContentLoaded', () => {
  const el = document.getElementById('app');
  if (el) {
    el.textContent = 'Hello';
  }
});

2. Using window.onload when DOMContentLoaded is enough

window.onload = function () {
  console.log('Runs later than necessary');
};

Problem:

  • load waits for more resources than many scripts actually need.

Prefer:

document.addEventListener('DOMContentLoaded', () => {
  console.log('Runs as soon as the DOM is ready');
});

3. Overwriting another onload handler

Broken code:

window.onload = function () {
  console.log('First handler');
};

window.onload = function () {
  console.log('Second handler');
};

Problem:

  • The second assignment replaces the first.

Fix:

window.addEventListener('load', () => {
  console.log('First handler');
});

window.addEventListener('load', () => {
  console.log('Second handler');
});

4. Forgetting null checks

Broken code:

document.addEventListener('DOMContentLoaded', () => {
  document.querySelector('.menu').classList.add('open');
});

Problem:

  • If .menu is missing, the code throws an error.

Fix:

document.addEventListener('DOMContentLoaded', () => {
  const menu = document.querySelector('.menu');
  if (!menu) return;
  menu.classList.add('open');
});

5. Assuming defer and async behave the same

  • defer waits until HTML parsing is done before execution.
  • async runs as soon as the script finishes downloading, which may be before the DOM is ready.

If DOM order matters, defer is usually safer than async.

Comparisons

ApproachWhen it runsBest forNotes
document.addEventListener('DOMContentLoaded', ...)After HTML is parsedAccessing DOM elementsMost common for DOM setup
window.addEventListener('load', ...)After full page and resources loadImage sizes, asset-dependent logicRuns later
Script at end of <body>After most HTML is parsedSimple pagesEasy but less explicit
<script defer>After HTML parsing completesExternal scripts in <head>Great modern default
<script async>As soon as script downloadsIndependent scripts like analyticsNot reliable for DOM-dependent order

DOMContentLoaded vs load

  • Use DOMContentLoaded if you need the document structure.
  • Use load if you need everything, including images and other resources.

defer vs putting script at the bottom

Both help avoid early DOM access.

  • defer keeps script tags in <head> and preserves cleaner HTML structure.
  • Bottom-of-body scripts are simple and still widely understood.

onload property vs addEventListener

TechniqueCan attach multiple handlers?Recommended?
window.onload = fnNo, later assignment overwrites earlier oneUsually no
window.addEventListener('load', fn)YesYes
Take Quiz

Cheat Sheet

Quick reference

Run code when DOM is ready

document.addEventListener('DOMContentLoaded', () => {
  // safe to access DOM elements
});

Run code when entire page is loaded

window.addEventListener('load', () => {
  // images and other resources are loaded too
});

Load external script in <head> safely

<script src="app.js" defer></script>

Rules of thumb

  • Need HTML elements? Use DOMContentLoaded.
  • Need images or full assets? Use load.
  • Using an external script in <head>? Prefer defer.
  • Want simple HTML? Put the script before </body>.
  • Need multiple event handlers? Use addEventListener, not onload = ....

Common error

const el = document.getElementById('item');
el.textContent = 'Hi';

If el is null, the page probably was not ready yet or the selector is wrong.

Safe pattern

document.addEventListener('DOMContentLoaded', () => {
  const el = document.getElementById('item');
  if (!el) return;
  el.textContent = 'Hi';
});

FAQ

What is the difference between DOMContentLoaded and load?

DOMContentLoaded fires when the HTML has been parsed and the DOM is ready. load fires later, when the whole page and its resources have finished loading.

Should I use DOMContentLoaded or window.onload?

Use DOMContentLoaded for most DOM-related tasks. Use window.onload only when you need images or other external resources to be fully loaded.

Is putting the script at the bottom of <body> still valid?

Yes. It is still a simple and valid way to make sure most of the DOM exists before the script runs.

Is defer better than DOMContentLoaded?

Not exactly better, but often convenient for external scripts in <head>. defer delays execution until after parsing. You can still use DOMContentLoaded inside deferred scripts if needed.

Why does my script work sometimes and fail other times?

This usually happens because of timing. If your script runs before the DOM elements exist, it may fail depending on browser speed, network timing, or page structure.

Can I use async for DOM-related scripts?

Usually not if the script depends on document structure or execution order. async scripts run as soon as they download, which can be too early.

Why is document.getElementById(...) returning null?

Common reasons are:

  • the element has not been parsed yet
  • the ID is incorrect
  • the script is running before the page is ready
Take Quiz

Mini Project

Description

Build a small page that updates a status message and attaches a click handler only after the DOM is ready. This demonstrates how to safely work with elements that appear in the <body> while the script is loaded from the <head>.

Goal

Create a page that waits for the document to be ready, updates text content, and responds to a button click without timing errors.

Requirements

  1. Load the JavaScript file from the <head>.
  2. Wait until the DOM is ready before accessing <body> elements.
  3. Update a status message on the page.
  4. Attach a click listener to a button.
  5. Show a result message when the button is clicked.
Take Quiz

Keep learning

Related questions

View more