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
DOMContentLoadedevent - listen for the
loadevent - place the script at the end of
<body> - use the
deferattribute 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:
nullelement 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.
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:
- The browser starts parsing the HTML.
- It reaches the
<script>inside<head>. - The script runs immediately.
- That script does not try to access
#boxyet. - Instead, it registers a listener for
DOMContentLoaded. - The browser continues parsing the rest of the page.
- It reaches
<body>and creates the<div id="box">element in the DOM. - Once the HTML is fully parsed, the browser fires
DOMContentLoaded. - The callback runs.
document.getElementById('box')now returns the real element.- The text changes from
Loading...toReady.
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.
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:
#appmay 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:
loadwaits 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
.menuis 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
deferwaits until HTML parsing is done before execution.asyncruns 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
| Approach | When it runs | Best for | Notes |
|---|---|---|---|
document.addEventListener('DOMContentLoaded', ...) | After HTML is parsed | Accessing DOM elements | Most common for DOM setup |
window.addEventListener('load', ...) | After full page and resources load | Image sizes, asset-dependent logic | Runs later |
Script at end of <body> | After most HTML is parsed | Simple pages | Easy but less explicit |
<script defer> | After HTML parsing completes | External scripts in <head> | Great modern default |
<script async> | As soon as script downloads | Independent scripts like analytics | Not reliable for DOM-dependent order |
DOMContentLoaded vs load
- Use
DOMContentLoadedif you need the document structure. - Use
loadif you need everything, including images and other resources.
defer vs putting script at the bottom
Both help avoid early DOM access.
deferkeeps script tags in<head>and preserves cleaner HTML structure.- Bottom-of-body scripts are simple and still widely understood.
onload property vs addEventListener
| Technique | Can attach multiple handlers? | Recommended? |
|---|---|---|
window.onload = fn | No, later assignment overwrites earlier one | Usually no |
window.addEventListener('load', fn) | Yes | Yes |
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>? Preferdefer. - Want simple HTML? Put the script before
</body>. - Need multiple event handlers? Use
addEventListener, notonload = ....
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
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
- Load the JavaScript file from the
<head>. - Wait until the DOM is ready before accessing
<body>elements. - Update a status message on the page.
- Attach a click listener to a button.
- Show a result message when the button is clicked.
Keep learning
Related questions
Allow Only Numeric Input (0-9) in HTML Input Using jQuery
Learn how to allow only digits 0-9 in an HTML input using jQuery, with examples, validation tips, common mistakes, and best practices.
Apply CSS !important with jQuery .css()
Learn why jQuery .css() cannot set !important and how to use CSSStyleDeclaration.setProperty() to apply inline important styles safely.
CSS ::before and ::after on Input Elements
Learn why CSS ::before and ::after usually do not work on input elements, what replaced elements are, and practical alternatives.