SharePointVault ~/gotchas/spfx ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ cached-rejected-dynamic-import.md
sharepoint-vault›gotchas›spfx›cached-rejected-dynamic-import.mdhistoryedit on GitHub
● GOTCHAspfx· 3 min read

A module-level cached import() caches the rejection too

applies to: any webpack/TypeScript app using dynamic import() for lazy chunks (SPFx, React, etc.)reviewed: 2026-07-24#spfx#webpack#lazy-loading#dynamic-import#error-handling

The common "load the library once" pattern — let p = null; if (!p) p = import('x') — caches the promise, and a rejected promise stays rejected forever. One transient chunk-load failure (flaky network, a CDN/Pages build not live yet) then disables the feature for the rest of the session, even after connectivity is back. Reset the cache to null in a .catch so the next call retries.

Oblíbený vzor „načti knihovnu jen jednou“ – let p = null; if (!p) p = import('x') – cachuje Promise, a jednou odmítnutý Promise zůstane odmítnutý navždy. Jediný přechodný výpadek načtení chunku (kolísavá síť, CDN/Pages build ještě není live) pak feature vypne do konce session, i když je síť zpět. Na .catch cache vynuluj na null, ať příští volání zkusí import znovu.

Symptom

A lazily-loaded library (diagram renderer, xlsx/docx parser, a heavy editor) works normally, but after a single blip — the user was briefly offline, or the freshly-deployed chunk hash wasn't live on the CDN yet — the feature stays broken for the entire session. Every subsequent attempt fails instantly with the same error, and only a hard page reload fixes it. The network tab shows no retry request on later attempts.

let libPromise: Promise<Lib> | null = null;

async function getLib(): Promise<Lib> {
  if (!libPromise) {
    libPromise = import(/* webpackChunkName: "lib" */ 'lib').then(m => init(m.default));
  }
  return libPromise;   // ← once this rejects, every future caller gets the same rejection
}

Cause

You're memoizing the promise, not its resolved value. A promise that rejects is a permanently-settled rejected promise — awaiting it again re-throws the same error without re-running anything. Because libPromise is now a non-null (rejected) promise, the if (!libPromise) guard is never true again, so import() is never retried. The dynamic import() itself would refetch the chunk on a fresh call (webpack doesn't cache a failed chunk load), but your guard never lets it.

Component-level retry (a React error state with a "try again" button that calls getLib() again) does not help while the module-level cache is poisoned — it hands back the same dead promise.

Fix

Clear the cache in a .catch and rethrow, so a failure is not memoized:

let libPromise: Promise<Lib> | null = null;

async function getLib(): Promise<Lib> {
  if (!libPromise) {
    libPromise = import(/* webpackChunkName: "lib" */ 'lib')
      .then(m => init(m.default))
      .catch(err => {
        libPromise = null;   // don't memoize a transient failure — let the next call retry
        throw err;           // still reject THIS call so the caller can show a fallback
      });
  }
  return libPromise;
}

The success path still caches (the library loads at most once); only failures reset. Callers that show a fallback on error now get a real retry the next time they ask.

Why it's easy to miss

  • On a fast, reliable dev network the import essentially never fails, so the poisoned-cache path never runs in testing.
  • The pattern looks obviously correct — "cache it so we only load once" — and the success case behaves perfectly.
  • It bites worst right after a deploy: the new chunk hash may be 404 for a minute (GitHub Pages / CDN build lag), every early visitor poisons the cache, and the feature looks dead until they reload.

See also

  • gotchas/spfx/shared-package-dynamic-import-inlines-with-commonjs.md — make sure the import() actually produces a lazy chunk (needs module: esnext) before you worry about caching it.
Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-07-24last updated: 2026-09-24historyedit on GitHubview source