Skip to content

Commit 7376416

Browse files
authored
Turbopack: align chunk loading error name (#86593)
- Set `error.name` just like Webpack does - Add some tests <img width="1854" height="451" alt="Bildschirmfoto 2025-11-28 um 10 44 06" src="https://github.com/user-attachments/assets/8cadd60e-2a7f-4715-9c39-66453fe400f4" /> <img width="1865" height="564" alt="Bildschirmfoto 2025-11-27 um 16 52 34" src="https://github.com/user-attachments/assets/959bdb52-83a5-4b73-9d5f-b727f8715a6e" />
1 parent 4390c7b commit 7376416

19 files changed

+212
-58
lines changed

.github/workflows/build_and_test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -910,10 +910,12 @@ jobs:
910910
export IS_WEBPACK_TEST=1
911911
912912
BROWSER_NAME=firefox node run-tests.js \
913-
test/production/pages-dir/production/test/index.test.ts
913+
test/production/pages-dir/production/test/index.test.ts \
914+
test/production/chunk-load-failure/chunk-load-failure.test.ts
914915
915916
NEXT_TEST_MODE=start BROWSER_NAME=safari node run-tests.js \
916917
test/production/pages-dir/production/test/index.test.ts \
918+
test/production/chunk-load-failure/chunk-load-failure.test.ts \
917919
test/e2e/basepath/basepath.test.ts \
918920
test/e2e/basepath/error-pages.test.ts
919921
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Async() {
2+
return 'this is a lazy loaded async component'
3+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client'
2+
3+
import dynamic from 'next/dynamic'
4+
import { Suspense } from 'react'
5+
6+
const Async = dynamic(() => import('./async'), { ssr: false })
7+
8+
export default function Page() {
9+
return (
10+
<>
11+
<Suspense fallback={<div>Loading...</div>}>
12+
<Async />
13+
</Suspense>
14+
</>
15+
)
16+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Layout({ children }) {
2+
return (
3+
<html lang="en">
4+
<head />
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client'
2+
3+
export default function Page() {
4+
return <>this is other</>
5+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { recursiveReadDir } from 'next/dist/lib/recursive-readdir'
3+
import path from 'path'
4+
import fs from 'fs'
5+
import { retry } from 'next-test-utils'
6+
7+
describe('chunk-load-failure', () => {
8+
const { next } = nextTestSetup({
9+
files: __dirname,
10+
})
11+
12+
async function getNextDynamicChunk() {
13+
const chunksPath = path.join(next.testDir, '.next/static/')
14+
const browserChunks = await recursiveReadDir(chunksPath, {
15+
pathnameFilter: (f) => /\.js$/.test(f),
16+
})
17+
let nextDynamicChunks = browserChunks.filter((f) =>
18+
fs
19+
.readFileSync(path.join(chunksPath, f), 'utf8')
20+
.includes('this is a lazy loaded async component')
21+
)
22+
expect(nextDynamicChunks).toHaveLength(1)
23+
24+
return nextDynamicChunks[0]
25+
}
26+
27+
it('should report async chunk load failures', async () => {
28+
let nextDynamicChunk = await getNextDynamicChunk()
29+
30+
let pageError: Error | undefined
31+
const browser = await next.browser('/dynamic', {
32+
beforePageLoad(page) {
33+
page.route('**/' + nextDynamicChunk, async (route) => {
34+
await route.abort('connectionreset')
35+
})
36+
page.on('pageerror', (error: Error) => {
37+
pageError = error
38+
})
39+
},
40+
})
41+
42+
await retry(async () => {
43+
const body = await browser.elementByCss('body')
44+
expect(await body.text()).toMatch(
45+
/Application error: a client-side exception has occurred while loading/
46+
)
47+
})
48+
49+
expect(pageError).toBeDefined()
50+
expect(pageError.name).toBe('ChunkLoadError')
51+
if (process.env.IS_TURBOPACK_TEST) {
52+
expect(pageError.message).toStartWith(
53+
'Failed to load chunk /_next/static/' + nextDynamicChunk
54+
)
55+
} else {
56+
expect(pageError.message).toMatch(/^Loading chunk \S+ failed./)
57+
expect(pageError.message).toContain('/_next/static/' + nextDynamicChunk)
58+
}
59+
})
60+
61+
it('should report aborted chunks when navigating away', async () => {
62+
let nextDynamicChunk = await getNextDynamicChunk()
63+
64+
let resolve
65+
try {
66+
const browser = await next.browser('/dynamic', {
67+
beforePageLoad(page) {
68+
page.route('**/' + nextDynamicChunk, async (route) => {
69+
// deterministically ensure that the async chunk is still loading during the navigation
70+
await new Promise((r) => {
71+
resolve = r
72+
})
73+
})
74+
page.on('pageerror', (error: Error) => {
75+
console.log('pageerror', error)
76+
})
77+
},
78+
})
79+
80+
await browser.get(next.url + '/other')
81+
82+
let body = await browser.elementByCss('body')
83+
expect(await body.text()).toMatch('this is other')
84+
85+
const browserLogs = (await browser.log()).filter(
86+
(m) => m.source === 'warning' || m.source === 'error'
87+
)
88+
89+
if (process.env.BROWSER_NAME === 'firefox') {
90+
expect(browserLogs).toContainEqual(
91+
expect.objectContaining({
92+
message: expect.stringContaining(
93+
'Loading failed for the <script> with source'
94+
),
95+
})
96+
)
97+
} else {
98+
// Chrome and Safari doesn't show any errors or warnings here
99+
expect(browserLogs).toBeEmpty()
100+
}
101+
} finally {
102+
// prevent hanging
103+
resolve?.()
104+
}
105+
})
106+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {}
3+
4+
export default nextConfig

turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/runtime-base.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ function loadChunkByUrlInternal(
250250
thenable,
251251
loadedChunk
252252
)
253-
entry = thenable.then(resolve).catch((error) => {
253+
entry = thenable.then(resolve).catch((cause) => {
254254
let loadReason: string
255255
switch (sourceType) {
256256
case SourceType.Runtime:
@@ -268,16 +268,14 @@ function loadChunkByUrlInternal(
268268
(sourceType) => `Unknown source type: ${sourceType}`
269269
)
270270
}
271-
throw new Error(
271+
let error = new Error(
272272
`Failed to load chunk ${chunkUrl} ${loadReason}${
273-
error ? `: ${error}` : ''
273+
cause ? `: ${cause}` : ''
274274
}`,
275-
error
276-
? {
277-
cause: error,
278-
}
279-
: undefined
275+
cause ? { cause } : undefined
280276
)
277+
error.name = 'ChunkLoadError'
278+
throw error
281279
})
282280
instrumentedBackendLoadChunks.set(thenable, entry)
283281
}

turbopack/crates/turbopack-ecmascript-runtime/js/src/nodejs/runtime.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,16 @@ function loadRuntimeChunkPath(
9999
const chunkModules: CompressedModuleFactories = require(resolved)
100100
installCompressedModuleFactories(chunkModules, 0, moduleFactories)
101101
loadedChunks.add(chunkPath)
102-
} catch (e) {
102+
} catch (cause) {
103103
let errorMessage = `Failed to load chunk ${chunkPath}`
104104

105105
if (sourcePath) {
106106
errorMessage += ` from runtime for chunk ${sourcePath}`
107107
}
108108

109-
throw new Error(errorMessage, {
110-
cause: e,
111-
})
109+
const error = new Error(errorMessage, { cause })
110+
error.name = 'ChunkLoadError'
111+
throw error
112112
}
113113
}
114114

@@ -133,15 +133,13 @@ function loadChunkAsync(
133133
const chunkModules: CompressedModuleFactories = require(resolved)
134134
installCompressedModuleFactories(chunkModules, 0, moduleFactories)
135135
entry = loadedChunk
136-
} catch (e) {
136+
} catch (cause) {
137137
const errorMessage = `Failed to load chunk ${chunkPath} from module ${this.m.id}`
138+
const error = new Error(errorMessage, { cause })
139+
error.name = 'ChunkLoadError'
138140

139141
// Cache the failure promise, future requests will also get this same rejection
140-
entry = Promise.reject(
141-
new Error(errorMessage, {
142-
cause: e,
143-
})
144-
)
142+
entry = Promise.reject(error)
145143
}
146144
chunkCache.set(chunkPath, entry)
147145
}

turbopack/crates/turbopack-tests/tests/snapshot/debug-ids/browser/output/aaf3a_crates_turbopack-tests_tests_snapshot_debug-ids_browser_input_index_0151fefb.js.map

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)