How to Add JavaScript to HTML

How to Add JavaScript to HTML

You have written your HTML. You have styled it with CSS. Now you want the page to do something — respond to a click, validate a form, load data from a server. That requires JavaScript, and the first question is always the same: where does it go?

There are three ways to attach JavaScript to an HTML document, plus a fourth option that changes how the browser loads it. Each one has a different effect on parsing, on the cascade of execution, and on how maintainable the codebase turns out to be.

This guide covers all of them: external files, internal script blocks, inline event handlers, and ES modules — along with script placement, the difference between async and defer, how to load scripts dynamically, what a Content Security Policy blocks, and the mistakes that make a script silently fail to run.

In one sentence: JavaScript is added to HTML with a <script> element — either pointing at an external file with src, or containing the code directly — and where you place it determines whether it blocks parsing or waits for the DOM.

The Three Methods at a Glance

Before the detail, here is the whole picture. All three methods run the same JavaScript. They differ in where the code lives and how much of the page it can reach.

Method 1

External file

A separate .js file linked with <script src="...">.

Reusable, cacheable, and the standard choice for every real project.

Method 2

Internal block

Code written directly inside a <script> element.

Applies to that one page only. Good for small scripts and experiments.

Method 3

Inline handler

JavaScript in an HTML attribute such as onclick="...".

Works, but mixes behavior into markup. Best avoided in new code.

And the fourth option, which is really a variation on the first two:

Modern

ES module

<script type="module"> — deferred by default, own scope, supports import.

The way modern applications are structured.

Avoid

Obsolete attributes

type="text/javascript", language="javascript", and javascript: URLs.

Unnecessary in HTML5 and best removed entirely.

The short version

Use an external file for almost everything, an internal block for a small script that belongs to one page, and an inline handler only when nothing else is possible.

The Script Element

Every method centers on the same element. Understanding its attributes makes the rest of the guide straightforward.

<script The element
src=”app.js” Points to an external file
defer Runs after parsing, in order
async Runs as soon as it downloads
type=”module” Treats it as an ES module
></script> Closing tag is required

Two attributes are worth noting for what they don’t need. The type attribute is unnecessary in HTML5 — JavaScript is the default. And the language attribute is long obsolete.

Obsolete — do not use
<script type="text/javascript" language="javascript" src="app.js"></script> <!-- Every attribute here is unnecessary or obsolete -->
Modern
<script src="app.js" defer></script> <!-- One attribute. That is all you need. -->
The closing tag is required

Unlike <link> or <img>, a <script> element cannot be self-closing. Writing <script src="app.js" /> causes the parser to treat everything after it as the script’s content, which breaks the rest of the page.

Method 1 — External File

An external script is a separate .js file connected to the HTML with the src attribute. It is how virtually every real project is built.

The two files

my-website/ ├── index.html ├── app.js ← the external script └── css/ └── styles.css

The HTML side

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My Page</title>
</head>
<body>
    <h1 id="title">Hello</h1>
    <button id="action">Click me</button>

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

The JavaScript side

The app.js file contains JavaScript and nothing else — no <script> tag, no HTML wrapper:

// app.js

const title  = document.getElementById('title');
const button = document.getElementById('action');

let clicks = 0;

button.addEventListener('click', function () {
    clicks = clicks + 1;
    title.textContent = 'Clicked ' + clicks + ' time' + (clicks === 1 ? '' : 's');
});

Relative and absolute paths

<!-- Same folder as the HTML file -->
<script src="app.js" defer></script>

<!-- Inside a subfolder -->
<script src="js/app.js" defer></script>

<!-- One folder up -->
<script src="../shared/utils.js" defer></script>

<!-- From the site root (leading slash) -->
<script src="/assets/js/app.js" defer></script>

<!-- From another domain -->
<script src="https://cdn.example.com/lib.js" defer></script>
A wrong path fails silently in the console

A missing file produces a 404 in the Network tab and a red error in the console, but the page itself still renders. If your script does not appear to run, check the Network tab first — it will show whether the browser even requested the file.

Why this is the default choice

Strengths

What you get

The browser caches the file once for the whole site, so every other page loads faster.

Structure and behavior stay separate — the HTML is clean and readable.

Editors provide syntax highlighting, linting, and autocomplete for a real .js file.

Trade-offs

What to watch for

One extra network request on the first visit, though it is cached afterward.

A wrong path produces no visual warning — only a console error.

Opening the HTML from file:// may block the request in some browsers.

Method 2 — Internal Script Block

An internal script writes the JavaScript directly inside a <script> element in the HTML document, with no src attribute.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Internal Script</title>
    <style>
        body {
            font-family: system-ui, sans-serif;
            padding: 32px;
            background: #f8fafc;
            text-align: center;
            margin: 0;
        }
        h1 { color: #0f172a; margin: 0 0 16px; }
        button {
            padding: 12px 24px;
            border: 0;
            border-radius: 10px;
            background: #0891b2;
            color: #fff;
            font: inherit;
            font-weight: 600;
            cursor: pointer;
        }
        button:hover { background: #0e7490; }
    </style>
</head>
<body>

    <h1 id="message">Click the button</h1>
    <button id="myButton">Click me</button>

    <script>
        // The code lives directly in the HTML document
        const heading = document.getElementById('message');
        const button  = document.getElementById('myButton');

        let clicks = 0;

        button.addEventListener('click', function () {
            clicks = clicks + 1;
            heading.textContent = 'You clicked ' + clicks + ' time' + (clicks === 1 ? '' : 's');
        });
    </script>

</body>
</html>

Notice that the content inside <script> is raw JavaScript — no escaping, no wrapping. The parser reads it exactly as it would read an external file.

Where it belongs

  • At the end of the body — the safest position. The DOM above it is already parsed.
  • In the head with defer — also safe, and runs after parsing.
  • In the head without defer — blocks parsing. Avoid unless the script must run before the page renders.
defer has no effect on an inline script

The defer attribute only applies to scripts with a src. An inline script has nothing to defer — it runs at the moment the parser reaches it, no matter what attributes it carries.

When internal scripts make sense

  • A single-page site. If there is only one page, a separate file adds a request for no benefit.
  • Quick experiments. One file, ready to share, no folder structure needed.
  • A small page-specific behaviour. A tiny script that only matters on this one document.
  • An anti-flash theme script. A few lines in the head that must run before the page renders.
Strengths

Why use it

No extra request — the code arrives with the HTML.

Everything is in one file, which is convenient for demos and single pages.

Useful for a script that must run before the body renders.

Trade-offs

What to watch for

The code cannot be reused by any other page on the site.

It is re-downloaded on every page load instead of being cached.

No separate syntax highlighting or linting from a real .js file.

Method 3 — Inline Event Handlers

An inline handler writes JavaScript directly in an HTML attribute, such as onclick or onmouseover. It is the oldest way to attach behavior, and the one to avoid in new code.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Inline Handlers</title>
    <style>
        body {
            font-family: system-ui, sans-serif;
            padding: 32px;
            background: #f8fafc;
            text-align: center;
            margin: 0;
        }
        h1 { color: #0f172a; margin: 0 0 16px; }
        button {
            padding: 12px 24px;
            border: 0;
            border-radius: 10px;
            background: #0891b2;
            color: #fff;
            font: inherit;
            font-weight: 600;
            cursor: pointer;
            margin: 0 4px;
        }
        button:hover { background: #0e7490; }
    </style>
</head>
<body>

    <h1 id="message">Click a button</h1>

    <!-- The JavaScript lives in the attribute -->
    <button onclick="document.getElementById('message').textContent = 'Hello!'">
        Say hello
    </button>

    <button onclick="changeColor()">
        Change color
    </button>

    <script>
        function changeColor() {
            const heading = document.getElementById('message');
            heading.style.color = heading.style.color === 'red' ? '#0f172a' : 'red';
        }
    </script>

</body>
</html>

Both buttons work. The first runs a statement directly; the second calls a function defined in a script block below. That second form — calling a named function — was the standard pattern for years.

Why inline handlers are discouraged

  • They mix behavior into markup. Presentation, structure, and behavior all end up in the same file, which undoes the separation the web is built on.
  • They cannot be cached. The code is part of the HTML response, so it downloads on every page load.
  • They are blocked by strict CSP. A Content Security Policy that forbids inline script disables every one of them with no error visible to the user.
  • They are hard to debug. A bug is a fragment of code buried in an attribute, not a named function you can search for.
  • They cannot be removed cleanly. Detaching an inline handler means editing the HTML, not calling a function.
  • Quoting gets ugly fast. Strings inside strings inside attributes require layered escaping.
Escaping quickly becomes unreadable

Writing a string inside an inline handler means escaping it for HTML, then escaping it again for JavaScript. Something as simple as setting a text value becomes a nest of quotes that is easy to get wrong and hard to read.

The modern replacement

Inline handler
<button onclick=" document.getElementById('msg') .textContent = 'Hi'" id="btn"> Click </button> <!-- Behavior is in the markup. Hard to find, hard to test, blocked by strict CSP. -->
addEventListener
<button id="btn">Click</button> // In a script: const btn = document.getElementById('btn'); const msg = document.getElementById('msg'); btn.addEventListener('click', function () { msg.textContent = 'Hi'; }); // Behavior is in JavaScript where // it belongs. One listener per element, // easy to find and easy to remove.
The one legitimate use

Inline handlers are sometimes used in HTML email, because email clients strip <script> blocks and ignore external files. Outside that niche, use addEventListener.

Where to Put the Script Tag

Placement determines whether a script delays parsing, and whether it can find the elements it needs. There are three positions, and they behave differently.

<head> (no defer)
1
Blocks parsing, runs early The parser stops at the script, downloads it, runs it, then continues. The DOM below does not exist yet, so any querySelector returns null.
end of <body>
2
Blocks at the end, but harmless The entire DOM is already parsed. The script runs immediately and can find every element. This was the classic recommendation.
<head> with defer
3
Best of both Downloads in parallel with parsing, runs after the DOM is complete. The page renders without waiting, and the script still finds every element.

See the difference

Head, no defer
parse head
download
run
parse body…
End of body
parse entire document
download
run
Head, defer
parse
parse continues in parallel with download…
run

The third row is the goal. Parsing runs uninterrupted, and the script runs the moment the DOM is ready. Nothing is delayed and nothing is missed.

<!-- Approach 1: at the end of the body -->
<body>
    <h1>Content</h1>
    <script src="app.js"></script>
</body>

<!-- Approach 2: in the head with defer -->
<head>
    <script src="app.js" defer></script>
</head>

<!-- Both are correct. The second is preferred because
     the download starts earlier. -->
Use defer and put it in the head

The browser discovers the file sooner, starts downloading in parallel with the HTML, and still waits until the DOM is ready before running it. There is no downside.

async vs defer vs Nothing

These three attributes control when a script runs relative to parsing. The differences matter most when you have several scripts that depend on each other.

Nothing

<script src=”app.js”>

Parser stops. Downloads. Runs. Then parsing resumes.

Order is guaranteed. Everything waits.

async

<script async src=”app.js”>

Downloads in parallel. Runs the moment it arrives — even mid-parse.

Order is not guaranteed.

defer

<script defer src=”app.js”>

Downloads in parallel. Runs after the DOM is fully parsed.

Order is guaranteed.

How each attribute behaves
Attribute Blocks parsing? When it runs Order preserved?
(none) Yes Immediately, at its position Yes
async No As soon as it downloads No
defer No After HTML parsing completes Yes
type="module" No After parsing, like defer Yes

Order matters with several scripts

<!-- With defer, the order is guaranteed -->
<script src="jquery.js" defer></script>
<script src="plugin.js" defer></script>
<script src="app.js" defer></script>
<!-- jquery runs, then plugin, then app -->

<!-- With async, the order depends on download speed -->
<script src="jquery.js" async></script>
<script src="plugin.js" async></script>
<script src="app.js" async></script>
<!-- If app.js finishes downloading first, it runs first —
     and crashes because jquery does not exist yet. -->
async is only safe for independent scripts

Use async for analytics, ads, and third-party widgets that nothing else depends on. For your own application code, use defer — it is faster than no attribute and safer than async.

Does the script need to touch the DOM? → defer
Does another script depend on this one? → defer
Is it a completely independent third-party script? → async
Must it run before the page renders? → inline in the head
Does it use import or export? → type=”module”

ES Modules

A script with type="module" is treated as an ES module. It is the modern way to structure JavaScript, and it changes several defaults at once.

<script type="module" src="app.js"></script>
// utils.js — exports
export function formatPrice(value) {
    return '$' + value.toFixed(2);
}

export const TAX_RATE = 0.2;

// app.js — imports
import { formatPrice, TAX_RATE } from './utils.js';

console.log(formatPrice(99.99 * (1 + TAX_RATE)));

What modules change

Benefits

What you get

Automatic defer — modules never block parsing.

Own scope — top-level variables do not leak to window.

Strict mode by default — silent bugs become errors.

Explicit dependencies — a file’s imports tell you what it needs.

Loaded once — a module imported by three files still executes only once.

Constraints

What to watch for

Must be served over HTTP — file:// is blocked by CORS.

Imports must include the extension: ./utils.js, not ./utils.

Each module is a separate request unless you bundle them.

Older browsers need a nomodule fallback.

Inline modules are also deferred

A module does not have to be an external file. An inline module gets the same treatment — deferred, strict, and scoped.

<script type="module">
    // Deferred automatically, strict mode, own scope
    const heading = document.querySelector('h1');
    heading.textContent = 'Loaded as a module';
</script>
The module waterfall

Modules are discovered by following imports. The browser fetches app.js, parses it, finds it imports utils.js, fetches that, finds its imports, and so on. Deeply nested modules can create a slow chain of requests, which is why production builds usually bundle everything into one file.

Loading a Script Dynamically

You can also add a script at runtime, from JavaScript itself. This is how analytics, third-party widgets, and lazy-loaded code are often handled.

// Create the element
const script = document.createElement('script');

// Set its source
script.src = 'https://example.com/widget.js';

// Optional: run it after the page has loaded
script.async = true;

// Optional: something to do when it arrives
script.onload = function () {
    console.log('Script loaded');
};

script.onerror = function () {
    console.error('Script failed to load');
};

// Attach it — the browser fetches and runs it
document.head.appendChild(script);

The moment the element is added to the document, the browser requests the file and runs it when it arrives. This is exactly what a static <script> tag does — you are just doing it later, from code.

Loading a script only when needed

function loadScript(src) {
    return new Promise(function (resolve, reject) {
        const script = document.createElement('script');
        script.src = src;
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
    });
}

// Load only when the user opens the chart panel
chartButton.addEventListener('click', async function () {
    await loadScript('https://cdn.example.com/charting.js');
    renderChart();   // now safe to call
});
Use dynamic imports for modules

For your own code, import() is cleaner than creating script elements. It returns a promise and works from anywhere: const { renderChart } = await import('./chart.js');

Content Security Policy

A Content Security Policy is an HTTP header that tells the browser which sources of code are allowed to run. It is a security feature — and it is the most common reason a script stops working on a site that used to load fine.

// A typical CSP header
Content-Security-Policy: script-src 'self' https://cdn.example.com

That header says: scripts may only load from your own domain and from cdn.example.com. Anything else is blocked, and the browser logs an error in the console.

What different CSP directives allow
Directive Effect
'self' Only scripts from the same origin as the page
'none' No scripts at all
'unsafe-inline' Allows inline <script> blocks and inline handlers
'unsafe-eval' Allows eval() and the Function constructor
https://cdn.example.com Allows scripts from that specific domain
'nonce-abc123' Allows one specific inline script carrying that nonce

Without 'unsafe-inline', every inline handler and inline <script> block is blocked — which is one of the strongest arguments against both. External files are unaffected.

The nonce approach

When you genuinely need one inline script — an anti-flash theme switcher in the head, for example — a CSP can whitelist exactly that one by nonce. The server generates a random value per request and adds it as an attribute on the script tag. Every other inline script stays blocked.

Common Mistakes

Each of these produces a script that silently does nothing, with the real cause hidden in the console. They are the ones that come up most often.

1. A wrong file path

Incorrect — file is in js/
<script src="app.js" defer></script> <!-- Browser looks in the project root. The file is actually at js/app.js. Result: 404, nothing runs. -->
Correct
<script src="js/app.js" defer></script>

Paths are relative to the HTML file, not to the project root. Check the Network tab — if the request is red, the path is wrong.

2. The script runs before the DOM exists

Incorrect
<head> <script src="app.js"></script> </head> <body> <h1 id="title">Hello</h1> </body> // app.js: const title = document.getElementById('title'); title.textContent = 'Changed';
TypeError: Cannot set properties of null The h1 has not been parsed yet, so getElementById returns null.

Three fixes, in order of preference: add defer, move the script to the end of the body, or wrap the code in a DOMContentLoaded listener.

3. Using a self-closing script tag

Incorrect — breaks the page
<script src="app.js" /> <p>This paragraph will not render.</p> <!-- The parser treats the paragraph as the script's content. Nothing after this point appears. -->
Correct
<script src="app.js"></script> <p>This paragraph renders normally.</p>

4. Adding type=”text/javascript”

Obsolete
<script type="text/javascript" src="app.js"> <!-- Works, but the attribute has been unnecessary since HTML5. -->
Modern
<script src="app.js" defer></script>

5. Inline handlers blocked by CSP

Incorrect — silently blocked
<button onclick="save()">Save</button> <!-- With a CSP lacking 'unsafe-inline', the click does nothing at all. The user sees no error. -->
Correct — external listener
<button id="save">Save</button> // in app.js document.getElementById('save') .addEventListener('click', save);

6. Loading a module from file://

Incorrect — CORS blocks it
<script type="module" src="app.js"></script> <!-- Opening index.html directly from the file system fails: Access to script at 'file:///...' from origin 'null' has been blocked by CORS policy -->
Correct — serve over HTTP
# Any local server works python3 -m http.server 8000 # then visit http://localhost:8000 <script type="module" src="app.js"></script>

7. Forgetting the file extension in an import

Incorrect
import { format } from './utils'; // Failed to resolve module specifier // './utils'. Relative references must // start with '/', './' or '../' — and // in the browser, include the extension.
Correct
import { format } from './utils.js';

8. Two scripts that both declare the same variable

Incorrect — a global name clash
<script src="a.js"></script> <script src="b.js"></script> // a.js const user = { name: 'Ada' }; // b.js const user = { name: 'Grace' }; // SyntaxError: Identifier 'user' // has already been declared
Correct — separate scopes
<script type="module" src="a.js"></script> <script type="module" src="b.js"></script> // Each module has its own scope. // No collision, ever.

9. Using async when order matters

Incorrect — race condition
<script src="jquery.js" async></script> <script src="plugin.js" async></script> // If plugin.js downloads first, it // runs first — and throws because // jQuery does not exist yet. // The failure is intermittent, // which makes it very hard to debug.
Correct — order guaranteed
<script src="jquery.js" defer></script> <script src="plugin.js" defer></script> // jquery always runs first, // then plugin. Every time.

10. Placing the script after the closing body tag

Incorrect — invalid placement
</body> <script src="app.js"></script> </html> // Browsers usually tolerate this and // move the script inside the body, // but it is invalid HTML and can // behave inconsistently.
Correct
<body> ... <script src="app.js"></script> </body> </html>
Read the console first

JavaScript, unlike CSS, reports every problem — a 404, a blocked script, a null reference, a name collision. Open DevTools, read the first red message, and it will almost always name the exact file and line.

Best Practices

These habits keep the connection between HTML and JavaScript clean as a project grows.

📁

Keep scripts in their own file

An external .js file, linked once, reused everywhere.

⏳

Use defer

It downloads in parallel, runs after parsing, and preserves order.

📂

Put scripts in a folder

A js/ directory keeps paths predictable and the root tidy.

🚫

Avoid inline handlers

Use addEventListener instead. It works with CSP and is testable.

📦

Bundle for production

Modules are great in development. Combine them at build time to avoid a request waterfall.

🔒

Never put secrets in client JS

Everything in a script file is visible to every visitor. API keys belong on the server.

🧩

Use modules for real projects

type="module" gives scope, strict mode, and explicit dependencies.

🏷️

Name files by purpose

gallery.js and checkout.js, not script2.js.

🔍

Check the Network tab first

Before debugging anything, confirm the file was actually requested and returned 200.

The one-line summary

One external file, linked with defer from the head, organized with addEventListener inside, and bundled for production.

Frequently Asked Questions

1. What are the ways to add JavaScript to HTML?

There are three main ways: an external file linked with the src attribute on a script tag, an internal script block written directly inside a <script> element, and inline event handlers written as attributes such as onclick. External files are the recommended approach for almost every project.

2. Where should I put the script tag?

At the end of the body, or in the head with the defer attribute. Either approach guarantees the HTML is parsed before the script runs. A plain script tag in the head without defer pauses parsing and delays the first paint.

3. What is the difference between async and defer?

Both download the script in parallel with parsing. defer waits until the HTML is fully parsed before running, and preserves the order of multiple scripts. async runs the script as soon as it finishes downloading, which means the order is not guaranteed.

4. Is type=”text/javascript” required?

No. In HTML5 the type attribute is unnecessary on a script tag because JavaScript is the default. The attribute was required in HTML4 and XHTML but should be omitted in modern code.

5. Why is my external script not working?

The most common causes are a wrong file path in the src attribute, a missing or misplaced script tag, the file being loaded before the DOM exists, or the script being blocked by a Content Security Policy. Check the browser console and the Network tab first.

6. Can I put a script tag in the body?

Yes. Placing the script at the end of the body is a common and valid pattern. It ensures the DOM is fully parsed before the script runs, which means the script can safely query any element on the page.

7. What is an inline event handler?

An inline event handler is JavaScript written directly in an HTML attribute such as onclick="doSomething()". It works, but it mixes behavior into markup, is hard to maintain, and is often blocked by Content Security Policy. Modern code uses addEventListener instead.

8. What is type=”module”?

A script with type="module" is treated as an ES module. It is deferred by default, runs in strict mode, has its own scope, and supports import and export statements. It is the modern way to structure JavaScript.

9. Why does my script run before the HTML exists?

A plain script tag in the head without defer runs immediately, before the parser has reached the body. Any querySelector call returns null, and the code throws a TypeError. Fix it by using defer, moving the script to the end of the body, or wrapping the code in a DOMContentLoaded listener.

10. Can I load a script dynamically?

Yes. Create a script element with document.createElement, set its src, and append it to the document. The browser fetches and runs it asynchronously. This is how analytics and third-party widgets are often loaded on demand.

11. What is a Content Security Policy and why does it block my script?

A Content Security Policy is an HTTP header that tells the browser which sources of scripts are allowed. If inline scripts or a particular domain are not whitelisted, the browser refuses to run them and logs an error in the console.

12. Should I use one large script file or several small ones?

In development, several small files organized by purpose are easier to work with. For production, a build step usually combines them into one file so the browser makes a single request. Loading many separate files directly in production slows the page down.

Key Takeaways

  • There are three ways to add JavaScript: external, internal, and inline handlers.
  • External files are the default choice — cached, reusable, and lintable.
  • A wrong src path fails silently except for a 404 in the Network tab.
  • Internal scripts belong at the end of the body, or in the head with defer.
  • Inline handlers mix behavior into markup and are blocked by strict CSP.
  • defer downloads in parallel and runs after parsing, in order.
  • async downloads in parallel and runs whenever it is ready — order not guaranteed.
  • type="module" is deferred by default, strict, and scoped.
  • Modules require the file extension in imports and will not load from file://.
  • Scripts can be loaded dynamically by creating a script element and appending it.
  • A Content Security Policy can block inline scripts and third-party domains.
  • The <script> element always needs a closing tag.

What to Learn Next

Now that the script is connected, the next step is the language itself. Start with JavaScript Syntax Explained and What Is JavaScript?.

From there, explore How JavaScript Works for the call stack and the event loop, and How JavaScript Runs in a Browser for the parsing and rendering pipeline. When you are ready to build, read Working with the DOM and JavaScript Events.

If you are still connecting the pieces, read HTML DOCTYPE, How to Add CSS to HTML, and HTML vs CSS for the surrounding context.

Practice Challenge

The fastest way to understand how scripts load is to try each method and watch the difference in DevTools. This exercise takes about twenty-five minutes.

  1. Create index.html and app.js in the same folder.
  2. In the HTML, add a heading with id="title" and a button with id="action".
  3. In app.js, write a click handler that updates the heading text with a counter.
  4. Link the script from the end of the body with no attributes. Confirm it works.
  5. Now move the script to the head with no attributes. Reload and read the console error. Note the exact message.
  6. Add defer to the head script. Confirm it works again, and note that the download starts earlier in the Network waterfall.
  7. Change defer to async. Reload a few times with the network throttled and observe that the timing varies.
  8. Add a second script utils.js that defines a function the first script calls. With async on both, watch it break intermittently. Switch both to defer and confirm the order is now fixed.
  9. Rewrite both files as ES modules with type="module" and an import statement. Confirm they still work.
  10. Break the import deliberately by removing the .js extension. Read the module resolution error.
  11. Now break the path: change src="app.js" to src="app.js2". Reload, open the Network tab, and find the 404.
  12. Finally, add an inline onclick handler to the button and confirm it works — then think about why you would not ship it that way.

If you complete those steps, you have used all three methods, watched a script fail because the DOM did not exist yet, seen async produce a race condition, fixed it with defer, converted everything to modules, and caused two different loading errors by hand. That sequence — connection, order, failure, fix — is the whole subject of adding JavaScript to HTML.

Inar Learn
Inar Learnhttps://inarlearn.com
Inar Learn is an innovative online learning platform offering high-quality courses, tutorials, and resources to help learners gain practical skills and grow their knowledge.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here