Dynamic import()
import() returns a module namespace promise — code splitting, conditional loads, and error handling for lazy routes.
- javascript
- dynamic-import
- modules
- code-splitting
Static import is hoisted and fixed at load time. import() is a function-like expression that loads a module on demand and returns a Promise of its namespace. Bundlers turn it into separate chunks — that’s route-based code splitting.
Basic form
const mod = await import('./utils/math.js');
mod.add(1, 2);
// named
const { add } = await import('./utils/math.js');
// default export lives on .default
const Chart = (await import('./Chart.js')).default;
import('./heavy.js')
.then((m) => m.init())
.catch((err) => console.error('chunk failed', err));
When to use it
| Scenario | Pattern |
|---|---|
| Route / page | load view module on navigation |
| Rare UI (modal, admin) | import on first open |
| Feature flag | import only if enabled |
| Locale data | import(./i18n/${locale}.js) — careful with bundler magic comments |
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
React.lazy style
// framework wraps import()
const Settings = React.lazy(() => import('./Settings.jsx'));
// bare equivalent
async function loadSettings() {
const mod = await import('./Settings.jsx');
return mod.default;
}
Always handle the loading and error states — network failures on chunks are real.
Paths and bundlers
// static string — best for bundler analysis
await import('./feature.js');
// dynamic path — bundler may include a whole directory context
const locale = 'en';
await import(`./messages/${locale}.json`);
Webpack/Vite use magic comments / glob rules. Fully dynamic import(userString) from arbitrary URLs is a security boundary — don’t load untrusted module URLs.
Caching
Modules are evaluated once per URL. Second import() of the same specifier resolves to the same exports (same module map entry).
const a = await import('./counter.js');
const b = await import('./counter.js');
a === b; // namespace objects may differ by identity in engines, but state is shared
a.inc === b.inc; // true — same binding
Top-level await in modules
Inside an ES module you can:
const config = await import('./config.js');
export const api = config.apiUrl;
That delays module evaluation (see top-level await).
Errors
try {
await import('./missing.js');
} catch (e) {
// network / 404 / syntax error in module
showFallback();
}
Prefetch when you can predict the need:
link.addEventListener(
'mouseenter',
() => {
import('./editor.js'); // warm cache
},
{ once: true },
);
Interview answer
“import() dynamically loads an ES module and returns a Promise of its namespace. Bundlers split it into async chunks for lazy routes and heavy features. Specifiers should be statically analyzable when possible; repeated imports share the module instance. Always handle rejection for failed chunk loads.”
Related
Named chunks and failure UX
const mod = await import(
/* webpackChunkName: "editor" */
'./editor.js'
);
Vite/Rollup use different comment/annotation conventions — check your bundler. On failure, show a retry control; chunk 404s happen after deploys when users hold old HTML pointing at hashed files that were purged.
async function loadEditor() {
try {
return await import('./editor.js');
} catch {
toast('Failed to load editor — refresh?');
throw new Error('chunk');
}
}
Prefetch on hover/focus for primary CTAs so the click feels instant without loading the chunk on every page view.
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.