Skip to content

fix(ui): auto-recover from ChunkLoadError after deployment#15944

Open
nancysangani wants to merge 1 commit intoargoproj:mainfrom
nancysangani:fix/chunk-loading-errors-15640
Open

fix(ui): auto-recover from ChunkLoadError after deployment#15944
nancysangani wants to merge 1 commit intoargoproj:mainfrom
nancysangani:fix/chunk-loading-errors-15640

Conversation

@nancysangani
Copy link
Copy Markdown
Contributor

@nancysangani nancysangani commented Apr 16, 2026

Summary

Fixes "Loading chunk X failed" errors by adding retry logic and error boundary for chunk loading failures.

Changes

  • Add lazyImport utility with retry mechanism
  • Add ChunkLoadErrorBoundary that auto-reloads on chunk errors
  • Add tests for both utilities

Fixes

Closes #15640

Summary by CodeRabbit

  • New Features

    • Added automatic recovery for chunk loading failures with user-friendly reload messaging.
    • Implemented retry logic for dynamic imports with incremental backoff delays.
  • Bug Fixes

    • Improved application stability by gracefully handling dynamic chunk load errors.

Copilot AI review requested due to automatic review settings April 16, 2026 15:53
@nancysangani nancysangani force-pushed the fix/chunk-loading-errors-15640 branch from 7689ebb to 23c2d7a Compare April 16, 2026 15:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to reduce end-user impact of post-deployment “Loading chunk X failed” errors by adding (1) a retryable lazy-import helper and (2) a chunk-load-specific error boundary that reloads the page when a chunk fails to load.

Changes:

  • Added a lazyImport helper with retry/backoff behavior for failed dynamic imports.
  • Added ChunkLoadErrorBoundary intended to auto-reload the app on chunk-load failures, and wired it into App.
  • Added Jest/RTL tests for the new utility and boundary.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
ui/src/shared/utils/lazy-import.ts New retry wrapper for dynamic imports (currently not used by any production code paths).
ui/src/shared/utils/lazy-import.test.ts Unit tests for retry behavior (timer/microtask ordering concerns).
ui/src/app/chunk-load-error-boundary.tsx New error boundary that reloads on chunk-load-like errors (needs safeguards and better composition).
ui/src/app/chunk-load-error-boundary.test.tsx Tests for boundary behavior (JSDOM window.location mocking + timer handling issues).
ui/src/app.tsx Wraps the app in the new boundary (but existing inner ErrorBoundary likely intercepts first).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +26
import {ComponentType} from 'react';

/**
* Lazy import wrapper that retries loading chunks on failure.
* Fixes "Loading chunk X failed" errors by attempting to reload the chunk.
* See: https://github.com/argoproj/argo-workflows/issues/15640
*/
export function lazyImport<T extends ComponentType<any>>(
importFunc: () => Promise<{default: T}>,
retries: number = 3,
delayMs: number = 1000
): Promise<{default: T}> {
let attemptCount = 0;

const attempt = (): Promise<{default: T}> => {
attemptCount++;
return importFunc().catch(error => {
if (attemptCount >= retries + 1) {
throw error;
}
return new Promise(resolve => setTimeout(() => resolve(attempt()), delayMs * attemptCount));
});
};

return attempt();
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lazyImport is currently unused in the UI codebase (only referenced by its own test). As-is, this PR adds retry logic but doesn’t apply it to any existing React.lazy(() => import(...)) call sites, so it won’t affect runtime chunk loading. Consider wiring this into the existing lazy imports or removing it until it’s actually used.

Suggested change
import {ComponentType} from 'react';
/**
* Lazy import wrapper that retries loading chunks on failure.
* Fixes "Loading chunk X failed" errors by attempting to reload the chunk.
* See: https://github.com/argoproj/argo-workflows/issues/15640
*/
export function lazyImport<T extends ComponentType<any>>(
importFunc: () => Promise<{default: T}>,
retries: number = 3,
delayMs: number = 1000
): Promise<{default: T}> {
let attemptCount = 0;
const attempt = (): Promise<{default: T}> => {
attemptCount++;
return importFunc().catch(error => {
if (attemptCount >= retries + 1) {
throw error;
}
return new Promise(resolve => setTimeout(() => resolve(attempt()), delayMs * attemptCount));
});
};
return attempt();
}
export {};

Copilot uses AI. Check for mistakes.
Comment thread ui/src/app.tsx
Comment on lines +26 to +30
<ChunkLoadErrorBoundary>
<Provider value={providerContext}>
<AppRouter history={history} notificationsManager={notificationsManager} popupManager={popupManager} />
</Provider>
</ChunkLoadErrorBoundary>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping the app in ChunkLoadErrorBoundary here likely won’t catch chunk-load failures in practice, because AppRouter already wraps the route Switch with the existing ErrorBoundary (which will intercept render errors first). As a result, chunk-load errors will still render the ErrorPanel instead of triggering the auto-reload. Consider moving ChunkLoadErrorBoundary inside AppRouter (around the Switch) or integrating chunk-load handling into the existing ErrorBoundary so it actually sees these errors.

Copilot uses AI. Check for mistakes.
Comment thread ui/src/shared/utils/lazy-import.ts Outdated
Comment on lines +17 to +21
return importFunc().catch(error => {
if (attemptCount >= retries + 1) {
throw error;
}
return new Promise(resolve => setTimeout(() => resolve(attempt()), delayMs * attemptCount));
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry loop retries on any import failure. That will delay surfacing real bugs (e.g., syntax/runtime errors in the imported module) and can cause repeated side effects if importFunc isn’t a pure dynamic import. Consider only retrying when the error matches a chunk-load/network transient (or accept a predicate) and immediately rethrow for other errors.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +31
// Only handle chunk load errors; re-throw others
if (ChunkLoadErrorBoundary.isChunkLoadError(error)) {
return {error, isReloading: true};
}
throw error;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing from getDerivedStateFromError for non-chunk errors is not a supported way to “bubble” errors to a parent boundary and can behave inconsistently across React versions. Prefer composing boundaries (e.g., nesting this inside the existing ErrorBoundary) or handling non-chunk errors with a fallback instead of throwing here.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +23
const reloadSpy = jest.fn();

beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: {reload: reloadSpy}
});
reloadSpy.mockClear();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redefining window.location like this is likely to throw in JSDOM (the location property is typically non-configurable). Prefer spying/mocking window.location.reload directly (without replacing window.location).

Suggested change
const reloadSpy = jest.fn();
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: {reload: reloadSpy}
});
reloadSpy.mockClear();
let reloadSpy: jest.SpyInstance;
beforeEach(() => {
reloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(() => {});

Copilot uses AI. Check for mistakes.
Comment thread ui/src/shared/utils/lazy-import.test.ts Outdated
Comment on lines +42 to +43
expect(reloadSpy).toHaveBeenCalledTimes(1);
expect(screen.getByText(/Reloading/i)).toBeTruthy();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation calls reload() inside setTimeout(..., 100), so asserting reloadSpy was called immediately after render(...) will fail unless timers are faked/advanced (and updates flushed). Advance timers before this assertion (wrap in act if needed).

Copilot uses AI. Check for mistakes.
Comment thread ui/src/app/chunk-load-error-boundary.tsx
Comment thread ui/src/app/chunk-load-error-boundary.test.tsx Outdated
@nancysangani nancysangani force-pushed the fix/chunk-loading-errors-15640 branch 4 times, most recently from 1b3c171 to e47bd42 Compare April 16, 2026 16:15
@nancysangani
Copy link
Copy Markdown
Contributor Author

/cc @Joibel

@nancysangani nancysangani force-pushed the fix/chunk-loading-errors-15640 branch from e47bd42 to 58d0f65 Compare April 16, 2026 16:23
@isubasinghe
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread ui/src/shared/utils/lazy-import.ts
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

A new ChunkLoadErrorBoundary error boundary component intercepts chunk-loading failures and triggers automatic page reloads, while a lazyImport utility provides retry logic with exponential backoff for dynamic imports. These are integrated into the app's top-level component to handle loading failures gracefully.

Changes

Cohort / File(s) Summary
Error Boundary Integration
ui/src/app.tsx
Wraps the app's <Provider> and <AppRouter> tree in a new <ChunkLoadErrorBoundary> component to catch chunk-load errors at the root level.
Error Boundary Implementation
ui/src/app/chunk-load-error-boundary.tsx, ui/src/app/chunk-load-error-boundary.test.tsx
New error boundary component that detects chunk-load errors by message inspection, logs errors, and triggers automatic window.location.reload() after 100ms. Renders "Reloading..." UI when catching errors. Includes test coverage for normal child rendering and error catching.
Lazy Import Utility
ui/src/shared/utils/lazy-import.ts, ui/src/shared/utils/lazy-import.test.ts
New lazyImport helper wrapping dynamic imports with retry logic (default 3 retries, 1000ms base delay). Implements incremental backoff between attempts and rejects with the final error if retries exhausted. Includes test verifying successful import resolution.

Sequence Diagram

sequenceDiagram
    participant User as User/Browser
    participant App as App Component
    participant Boundary as ChunkLoadErrorBoundary
    participant Module as Dynamic Module
    participant Reload as window.location

    User->>App: Load application
    App->>Boundary: Render with children
    Boundary->>Module: Attempt to load chunk
    Module-->>Boundary: Loading chunk 775 failed (error)
    Boundary->>Boundary: isChunkLoadError() checks error
    Boundary->>Boundary: getDerivedStateFromError() sets isReloading=true
    Boundary->>Boundary: componentDidCatch() logs error
    Boundary->>Boundary: Schedule reload after 100ms
    Boundary->>User: Render "Reloading..." UI
    Boundary->>Reload: window.location.reload()
    Reload->>User: Page reloads
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(ui): auto-recover from ChunkLoadError after deployment' directly and clearly describes the main change: adding auto-recovery from chunk load errors.
Description check ✅ Passed The description provides motivation, modifications, and references the fixed issue (#15640), but lacks testing verification details and deployment/documentation updates as suggested by the template.
Linked Issues check ✅ Passed The pull request successfully addresses issue #15640 by implementing retry logic via lazyImport and automatic page reload via ChunkLoadErrorBoundary when chunk load errors occur.
Out of Scope Changes check ✅ Passed All changes (lazyImport utility, ChunkLoadErrorBoundary component, and their tests) are directly scoped to fixing chunk loading failures; no out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
ui/src/shared/utils/lazy-import.test.ts (1)

7-13: Cover the retry and final-failure paths.

Line 8 only verifies the success path, but the PR’s main behavior is retrying failed dynamic imports. Add fake-timer tests that assert importFunc is retried after failures and rejects after the configured retry budget.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/shared/utils/lazy-import.test.ts` around lines 7 - 13, Add tests for
lazyImport's retry and final-failure paths: write two fake-timer tests using
jest.useFakeTimers() that mock the dynamic import function (importFunc) to first
reject a number of times then resolve, and assert lazyImport retries by checking
mock call count after advancing timers (use jest.advanceTimersByTime or
runAllTimers) and that the returned value is eventually the module when within
the retry budget; also add a test where importFunc always rejects and assert
lazyImport rejects after the configured retry budget (check thrown error and
final call count matches maxRetries), referencing lazyImport and the import
function mock to locate the logic and ensuring timers are advanced between retry
intervals to trigger retries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/src/app.tsx`:
- Around line 9-10: The import order in app.tsx is wrong: move the
ChunkLoadErrorBoundary import so it comes after the AppRouter import to match
project ordering rules; specifically update the import sequence so AppRouter is
imported before ChunkLoadErrorBoundary (references: AppRouter and
ChunkLoadErrorBoundary in app.tsx) and save formatting so CI passes.
- Around line 25-30: Remove the outer ChunkLoadErrorBoundary that currently
wraps Provider and AppRouter in app.tsx and instead wrap only the route Switch
inside the existing ErrorBoundary in app-router.tsx: update app.tsx to render
Provider -> AppRouter (no ChunkLoadErrorBoundary) and update app-router.tsx to
place ChunkLoadErrorBoundary directly around the Switch (inside the existing
ErrorBoundary) so that ChunkLoadErrorBoundary catches lazy chunk load failures
first while the outer ErrorBoundary continues to handle non-chunk errors.

In `@ui/src/app/chunk-load-error-boundary.test.tsx`:
- Around line 19-28: The test triggers ChunkLoadErrorBoundary's error handler
which schedules window.location.reload via setTimeout; to avoid side effects use
jest.useFakeTimers() at the start of the test, mock or spy on
window.location.reload (e.g., jest.spyOn(window, 'location', 'get') or replace
reload with a jest.fn()), render the ThrowError component inside
ChunkLoadErrorBoundary, advance timers with jest.advanceTimersByTime(100) (or
jest.runAllTimers()) and then assert the mocked reload was called, and finally
restore timers and the mock to avoid affecting other tests.

In `@ui/src/app/chunk-load-error-boundary.tsx`:
- Around line 16-43: The chunk-load detection is too broad and the unconditional
reload in componentDidCatch causes potential 100ms refresh loops; narrow
isChunkLoadError to only detect chunk/dynamic-import errors (e.g., check
error.name === 'ChunkLoadError' and message patterns like 'Loading chunk' or
'dynamic import' but remove generic 'Failed to fetch'/'NetworkError'), and
change retry logic to be one-shot using state/flag: update
getDerivedStateFromError to set a retry flag (e.g., isReloading or hasRetried)
only when a chunk error is detected, and in componentDidCatch only call
window.location.reload() if the retry flag is false then set hasRetried=true
(persisted in component state or sessionStorage) so subsequent errors render a
fallback UI instead of reloading repeatedly; ensure getDerivedStateFromError
re-throws non-chunk errors as before.

In `@ui/src/shared/utils/lazy-import.ts`:
- Around line 8-12: The function signature for lazyImport is not formatted to
the project's formatter output; update the declaration of lazyImport so its
entire signature is on a single line (including generic, parameters importFunc,
retries, delayMs and return type Promise<{default: T}>), matching the
CI-reported formatter shape; locate the lazyImport function and replace the
current multi-line signature with the formatter-aligned one-line signature and
commit the change.
- Around line 3-6: Import the lazyImport helper from
ui/src/shared/utils/lazy-import and use it to wrap the dynamic import factories
used with React.lazy at each lazy-loading call site: replace React.lazy(() =>
import('...')) with React.lazy(lazyImport(() => import('...'))) in the lazy
React imports inside full-height-logs-viewer (FullHeightLogsViewer),
suspense-monaco-editor (SuspenseMonacoEditor), suspense-react-markdown-gfm
(SuspenseReactMarkdownGfm) and report-container (ReportContainer) so the retry
logic in lazyImport is actually invoked when chunks fail to load.

---

Nitpick comments:
In `@ui/src/shared/utils/lazy-import.test.ts`:
- Around line 7-13: Add tests for lazyImport's retry and final-failure paths:
write two fake-timer tests using jest.useFakeTimers() that mock the dynamic
import function (importFunc) to first reject a number of times then resolve, and
assert lazyImport retries by checking mock call count after advancing timers
(use jest.advanceTimersByTime or runAllTimers) and that the returned value is
eventually the module when within the retry budget; also add a test where
importFunc always rejects and assert lazyImport rejects after the configured
retry budget (check thrown error and final call count matches maxRetries),
referencing lazyImport and the import function mock to locate the logic and
ensuring timers are advanced between retry intervals to trigger retries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 85865b81-8223-48d2-a173-97a31c6d0f63

📥 Commits

Reviewing files that changed from the base of the PR and between 51ea54b and 58d0f65.

📒 Files selected for processing (5)
  • ui/src/app.tsx
  • ui/src/app/chunk-load-error-boundary.test.tsx
  • ui/src/app/chunk-load-error-boundary.tsx
  • ui/src/shared/utils/lazy-import.test.ts
  • ui/src/shared/utils/lazy-import.ts

Comment thread ui/src/app.tsx
Comment thread ui/src/app.tsx
Comment thread ui/src/app/chunk-load-error-boundary.test.tsx
Comment thread ui/src/app/chunk-load-error-boundary.tsx
Comment thread ui/src/shared/utils/lazy-import.ts
Comment thread ui/src/shared/utils/lazy-import.ts
Signed-off-by: Nancy <9d.24.nancy.sangani@gmail.com>
@nancysangani nancysangani force-pushed the fix/chunk-loading-errors-15640 branch from 58d0f65 to e9eb250 Compare April 22, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error "Loading chunk 775 failed"

3 participants