What is the difference between async and defer?
async vs defer - two HTML script attributes that both download scripts in parallel with HTML parsing, but differ in when execution happens: async runs the moment the download finishes, defer waits until the HTML parser is done.
Theory
TL;DR
asyncis like ordering food and eating it the second it arrives, no matter what else is happening at the tabledeferis like finishing your main course first, then eating dessert in the order you ordered it- Main difference:
asynccan interrupt HTML parsing to execute;defernever does - Use
deferfor scripts that touch the DOM or depend on other scripts. Useasyncfor independent third-party scripts like analytics
Quick example
<!-- Blocks HTML parsing: downloads AND executes before parser continues -->
<script src="app.js"></script>
<!-- Downloads in parallel, executes immediately when ready (may interrupt parsing) -->
<script src="analytics.js" async></script>
<!-- Downloads in parallel, executes after HTML parsing, order preserved -->
<script src="jquery.js" defer></script>
<script src="app.js" defer></script>Both async and defer start downloading immediately and let the HTML parser keep working. The difference shows up at execution time.
Key difference
async executes the moment the script finishes downloading. If parsing is still in progress, the browser pauses it, runs the script, then resumes. Two async scripts can execute in any order depending on which downloads faster. defer queues the script and only runs it after the HTML parser finishes, in document order. That makes defer predictable for anything that depends on other scripts or on the DOM existing.
When to use
defer- scripts that manipulate the DOM, depend on other scripts (React bundles, jQuery plugins, framework init code, analytics that reads page content)async- third-party scripts that don't depend on your code and don't need the DOM (Google Analytics, Sentry, ad networks, chat widgets)- Neither - inline scripts (
deferis ignored on inline code), or ES modules (type="module"is already deferred by spec)
Comparison table
| Aspect | Default <script> | async | defer |
|---|---|---|---|
| Download | Blocks parsing | Parallel | Parallel |
| Execution | Blocks parsing | Immediate (may interrupt) | After parsing |
| Order preserved | N/A | No | Yes |
| DOM ready at execution | No | No | Yes |
| Best for | Critical inline code | Independent third-party | App code, dependencies |
| When to use | Rare | Analytics, ads, tracking | 95% of your scripts |
How the browser handles this
When the HTML parser hits a <script> tag, it enters a decision point. With no attribute, it stops, downloads, executes, then continues. With async, it spawns a network request and keeps parsing, but the moment that script arrives, parsing stops and the script executes. With defer, it spawns the same parallel request but only executes after the parser finishes, right before the DOMContentLoaded event fires. Deferred scripts run in document order; async scripts run in download-completion order, which is non-deterministic.
One thing I have seen trip up teams: they assume async is always faster. For independent scripts, it is. But if your script needs the DOM, async can execute it before the expected elements exist. You get runtime errors that appear and disappear based on network speed, which makes them frustrating to reproduce.
Common mistakes
Mistake 1: Using async for dependent scripts
<!-- Wrong: jquery-plugin.js might execute before jquery.js -->
<script src="jquery.js" async></script>
<script src="jquery-plugin.js" async></script>
<!-- Fix: defer preserves order -->
<script src="jquery.js" defer></script>
<script src="jquery-plugin.js" defer></script>If jquery-plugin.js downloads faster, it executes first. You get $ is not defined at runtime, and the bug disappears on slower connections. Hard to reproduce, harder to explain to a client.
Mistake 2: Both async and defer on the same script
<!-- Wrong: async takes precedence, defer is silently ignored -->
<script src="app.js" async defer></script>
<!-- Fix: pick one -->
<script src="app.js" defer></script>The browser treats async defer as just async. This combination was a fallback for very old browsers that did not support async, but those browsers are gone. Avoid it today.
Mistake 3: Adding defer to inline scripts
<!-- Wrong: defer has no effect on inline scripts -->
<script defer>
console.log(document.getElementById('root')); // Still null
</script>
<!-- Fix: move to an external file -->
<script src="app.js" defer></script>The spec says defer only applies to external scripts with a src attribute. Inline scripts ignore it entirely and execute immediately.
Mistake 4: Adding defer to module scripts
<!-- Redundant: type="module" is already deferred by spec -->
<script type="module" src="app.js" defer></script>
<!-- Same behavior, defer attribute unnecessary -->
<script type="module" src="app.js"></script>Mistake 5: Expecting DOMContentLoaded to fire inside a deferred script
Deferred scripts run before DOMContentLoaded fires, not after. The DOM is ready, but the event has not been dispatched yet. If your deferred script listens for DOMContentLoaded, that handler will never fire because the event already passed by the time the listener registers.
Real-world usage
- React / Vue / Angular bundles -
defer(Next.js, Nuxt, Angular CLI all default to this) - Google Analytics -
async(independent, does not need the DOM) - Sentry error tracking -
async(captures errors independently) - jQuery + plugins -
deferon both (preserves load order) - Stripe / PayPal checkout -
async(third-party, self-contained) - Custom analytics that reads page content -
defer(needs DOM to exist)
Follow-up questions
Q: What happens if a deferred script throws an error?
A: That script stops executing, but subsequent deferred scripts still run. The page does not break by itself, but any script that depended on the errored one may fail silently.
Q: What is the difference between defer and putting scripts at the end of </body>?
A: Functionally similar for execution order. But defer in <head> starts downloading earlier, during HTML parsing, so the script is ready sooner. Putting scripts at the end of body was the old workaround before defer was well-supported. Today, defer in <head> is the correct pattern.
Q: Can you use async with type="module"?
A: Yes. But modules are already deferred by default, so adding async makes them execute immediately on download like classic async scripts. That breaks the dependency order modules normally preserve. Rarely useful in practice.
Q: You have three scripts: A depends on B, B is independent, C depends on A. How do you load them optimally?
A: Load B with async since it has no dependencies. Load A and C with defer to guarantee A runs before C. B downloads in parallel and executes whenever ready; A and C execute in document order after parsing. If B is actually required by A, use defer for all three to be safe.
Examples
Basic: async interrupting DOM access
<head>
<script src="analytics.js" async></script>
</head>
<body>
<div id="app">Loading...</div>
</body>// analytics.js
// This might run before <div id="app"> is parsed
const app = document.getElementById('app');
console.log(app); // null if HTML is not fully parsed yetanalytics.js starts downloading the moment the browser sees the <script> tag. On a fast connection it can finish downloading before the body is parsed, so getElementById returns null. Switching to defer makes the read safe because execution waits for parsing to finish.
Intermediate: React initialization with defer
<head>
<script src="react.js" defer></script>
<script src="react-dom.js" defer></script>
<script src="app.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>// app.js - runs after HTML parsing, after react.js and react-dom.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);All three scripts download in parallel during HTML parsing. They execute in order: react.js first, then react-dom.js, then app.js. By the time app.js runs, ReactDOM is available and #root exists in the DOM. This is exactly what Next.js generates by default for every page bundle.
Advanced: Race condition with async
// The bug: two async scripts with a dependency
// <script src="jquery.js" async></script>
// <script src="jquery-plugin.js" async></script>
// Timeline on a fast CDN connection:
// t=0ms: both scripts start downloading
// t=40ms: jquery-plugin.js arrives (smaller file) -> executes -> ERROR: $ is not defined
// t=80ms: jquery.js arrives -> executes, but plugin already failed
// Timeline on a slow connection:
// t=0ms: both scripts start downloading
// t=200ms: jquery.js arrives first due to network reordering -> executes
// t=250ms: jquery-plugin.js arrives -> $ exists -> works perfectly
// The bug only appears on fast connections. Passes local testing, breaks on CDN.
// Fix: use defer to guarantee order
// <script src="jquery.js" defer></script>
// <script src="jquery-plugin.js" defer></script>
// Analytics can still be async because it has no dependencies
// <script src="google-analytics.js" async></script>This is the kind of bug that passes every test locally and breaks in production at scale. Async execution order depends on which file bytes arrive first, which changes based on network conditions, CDN edge location, and file size. defer removes that variable entirely.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.