Skip to content

Refactor extractExportedConstValue to return { value } | null instead of throwing#90510

Open
sokra wants to merge 2 commits intocanaryfrom
sokra/avoid-throw
Open

Refactor extractExportedConstValue to return { value } | null instead of throwing#90510
sokra wants to merge 2 commits intocanaryfrom
sokra/avoid-throw

Conversation

@sokra
Copy link
Member

@sokra sokra commented Feb 25, 2026

Summary

Refactors extractExportedConstValue and the internal extractValue function to use return values instead of exception-based control flow, improving performance during build analysis.

Commit 1: Refactor extractExportedConstValue

  • Returns { value: any } | null instead of throwing NoSuchDeclarationError
  • null when the exported const declaration is not found, { value } when found
  • Removes NoSuchDeclarationError class

Commit 2: Refactor extractValue

  • Returns ExtractValueResult ({ value: any } | { unsupported: string; path?: string }) instead of throwing UnsupportedValueError
  • Extracts path-formatting logic from UnsupportedValueError constructor into a standalone formatCodePath helper
  • All 6 throw sites become return statements; 3 recursive call sites propagate unsupported results
  • Removes UnsupportedValueError class entirely
  • Removes all try/catch blocks from the 4 callsites in get-page-static-info.ts
  • Updates warnAboutUnsupportedValue to accept the result type directly

Motivation

Both extractExportedConstValue and extractValue used throw/catch for expected control flow — a missing declaration and unsupported AST node types are normal cases during build analysis, not exceptional errors. Every page/route processed during build hits these functions multiple times. Constructing Error objects (which capture stack traces) and unwinding the stack is expensive in V8. Returning discriminated result types avoids this overhead entirely.

Test Plan

  • Existing unit tests pass (test/unit/parse-page-static-info.test.ts — 5/5)
  • No behavioral change: callsites produce identical warnings/errors for unsupported values and silently skip missing declarations, same as before

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 25, 2026

Failing test suites

Commit: 9f9107f | About building and testing Next.js

pnpm test-start test/e2e/url-imports/url-imports.test.ts (job)

  • Handle url imports > should render the /static page (DD)
  • Handle url imports > should client-render the /static page (DD)
  • Handle url imports > should render the /ssr page (DD)
  • Handle url imports > should client-render the /ssr page (DD)
  • Handle url imports > should render the /ssg page (DD)
  • Handle url imports > should client-render the /ssg page (DD)
  • Handle url imports > should render a static url image import (DD)
  • Handle url imports > should allow url import in css (DD)
  • Handle url imports > should respond on value api (DD)
Expand output

● Handle url imports › should render the /static page

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should client-render the /static page

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should render the /ssr page

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should client-render the /ssr page

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should render the /ssg page

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should client-render the /ssg page

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should render a static url image import

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should allow url import in css

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Handle url imports › should respond on value api

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

pnpm test-start-turbo test/production/emit-decorator-metadata/index.test.ts (turbopack) (job)

  • emitDecoratorMetadata SWC option > should compile with emitDecoratorMetadata enabled (DD)
  • emitDecoratorMetadata SWC option > should compile with emitDecoratorMetadata enabled for API (DD)
Expand output

● emitDecoratorMetadata SWC option › should compile with emitDecoratorMetadata enabled

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● emitDecoratorMetadata SWC option › should compile with emitDecoratorMetadata enabled for API

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● Test suite failed to run

TypeError: Cannot read properties of undefined (reading 'destroy')

  19 |   })
  20 |
> 21 |   afterAll(() => next.destroy())
     |                       ^
  22 |
  23 |   it('should compile with emitDecoratorMetadata enabled', async () => {
  24 |     let browser: Playwright

  at Object.destroy (production/emit-decorator-metadata/index.test.ts:21:23)

pnpm test test/integration/telemetry/test/index.test.ts (job)

  • Telemetry CLI > production mode > cli session: babel tooling config (DD)
  • Telemetry CLI > production mode > cli session: custom babel config (plugin) (DD)
  • Telemetry CLI > production mode > cli session: package.json custom babel config (plugin) (DD)
  • Telemetry CLI > production mode > cli session: custom babel config (preset) (DD)
  • Telemetry CLI > production mode > cli session: next config with webpack (DD)
  • Telemetry CLI > production mode > detect static 404 correctly for next build (DD)
  • Telemetry CLI > production mode > detect page counts correctly for next build (DD)
Expand output

● Telemetry CLI › production mode › cli session: babel tooling config

thrown: "Exceeded timeout of 60000 ms for a test.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."

  188 |       })
  189 |
> 190 |       it('cli session: babel tooling config', async () => {
      |       ^
  191 |         await fs.rename(
  192 |           path.join(appDir, '.babelrc.default'),
  193 |           path.join(appDir, '.babelrc')

  at it (integration/telemetry/test/index.test.ts:190:7)
  at integration/telemetry/test/index.test.ts:98:56
  at Object.describe (integration/telemetry/test/index.test.ts:13:1)

● Telemetry CLI › production mode › cli session: custom babel config (plugin)

TypeError: Cannot read properties of null (reading 'pop')

  231 |
  232 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 233 |           .exec(stderr)
      |                 ^
  234 |           .pop()
  235 |
  236 |         expect(event).toMatch(/"hasNextConfig": false/)

  at Object.stderr (integration/telemetry/test/index.test.ts:233:17)

● Telemetry CLI › production mode › cli session: package.json custom babel config (plugin)

TypeError: Cannot read properties of null (reading 'pop')

  257 |
  258 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 259 |           .exec(stderr)
      |                 ^
  260 |           .pop()
  261 |
  262 |         expect(event).toMatch(/"hasNextConfig": false/)

  at Object.stderr (integration/telemetry/test/index.test.ts:259:17)

● Telemetry CLI › production mode › cli session: custom babel config (preset)

TypeError: Cannot read properties of null (reading 'pop')

  283 |
  284 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 285 |           .exec(stderr)
      |                 ^
  286 |           .pop()
  287 |
  288 |         expect(event).toMatch(/"hasNextConfig": false/)

  at Object.stderr (integration/telemetry/test/index.test.ts:285:17)

● Telemetry CLI › production mode › cli session: next config with webpack

TypeError: Cannot read properties of null (reading 'pop')

  309 |
  310 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 311 |           .exec(stderr)
      |                 ^
  312 |           .pop()
  313 |
  314 |         expect(event).toMatch(/"hasNextConfig": true/)

  at Object.stderr (integration/telemetry/test/index.test.ts:311:17)

● Telemetry CLI › production mode › detect static 404 correctly for next build

TypeError: Cannot read properties of null (reading 'pop')

  342 |
  343 |         const event1 = /NEXT_BUILD_OPTIMIZED[\s\S]+?{([\s\S]+?)}/
> 344 |           .exec(stderr)
      |                 ^
  345 |           .pop()
  346 |         expect(event1).toMatch(/hasStatic404.*?true/)
  347 |       })

  at Object.stderr (integration/telemetry/test/index.test.ts:344:17)

● Telemetry CLI › production mode › detect page counts correctly for next build

TypeError: Cannot read properties of null (reading 'pop')

  354 |
  355 |         const event1 = /NEXT_BUILD_OPTIMIZED[\s\S]+?{([\s\S]+?)}/
> 356 |           .exec(stderr)
      |                 ^
  357 |           .pop()
  358 |         expect(event1).toMatch(/"staticPropsPageCount": 2/)
  359 |         expect(event1).toMatch(/"serverPropsPageCount": 2/)

  at Object.stderr (integration/telemetry/test/index.test.ts:356:17)

@sokra sokra requested a review from timneutkens February 25, 2026 12:01
@sokra sokra marked this pull request as ready for review February 25, 2026 12:01
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 25, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁█▁▁▁
Cold (Ready in log) 438ms 440ms ▁█▂▂▁
Cold (First Request) 1.250s 1.261s ▁█▄▄▄
Warm (Listen) 456ms 456ms ▁█▁▁▁
Warm (Ready in log) 444ms 444ms ▁█▁▁▁
Warm (First Request) 343ms 345ms ▁█▂▁▁
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 456ms 455ms ▁▅▁▅▁
Cold (Ready in log) 439ms 439ms █▆▅▆▂
Cold (First Request) 1.964s 1.963s ▆█▄▅▁
Warm (Listen) 456ms 456ms ▃▃▁▃▃
Warm (Ready in log) 438ms 439ms ▇▇▅▆▁
Warm (First Request) 1.956s 1.972s ▄█▄▄▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.947s 3.985s ▁█▁▁▁
Cached Build 3.910s 3.919s ▁█▁▁▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.067s 14.096s ▂█▂▂▁
Cached Build 14.164s 14.177s ▃█▂▂▁
node_modules Size 475 MB 475 MB ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **400 kB** → **400 kB** ✅ -13 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 764 B 764 B
Total 764 B 764 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 451 B 452 B
Total 451 B 452 B ⚠️ +1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 58.3 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 256 B 254 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 59 kB -
Total 232 kB 233 kB ⚠️ +719 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.5 kB 2.5 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.97 kB ✅ -2 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 254 kB 254 kB
Total 379 kB 379 kB ⚠️ +333 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 615 B 613 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.8 kB 44 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.4 kB 45.6 kB ⚠️ +205 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.01 MB 4.02 MB 🔴 +5.68 kB (+0%)
index.pack gzip 102 kB 102 kB
index.pack.old gzip 102 kB 102 kB
Total 4.22 MB 4.22 MB ⚠️ +5.94 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 320 kB 320 kB
app-page-exp..prod.js gzip 170 kB 170 kB
app-page-tur...dev.js gzip 319 kB 319 kB
app-page-tur..prod.js gzip 169 kB 169 kB
app-page-tur...dev.js gzip 316 kB 316 kB
app-page-tur..prod.js gzip 168 kB 168 kB
app-page.run...dev.js gzip 316 kB 316 kB
app-page.run..prod.js gzip 168 kB 168 kB
app-route-ex...dev.js gzip 70.8 kB 70.8 kB
app-route-ex..prod.js gzip 49.2 kB 49.2 kB
app-route-tu...dev.js gzip 70.8 kB 70.8 kB
app-route-tu..prod.js gzip 49.2 kB 49.2 kB
app-route-tu...dev.js gzip 70.4 kB 70.4 kB
app-route-tu..prod.js gzip 49 kB 49 kB
app-route.ru...dev.js gzip 70.4 kB 70.4 kB
app-route.ru..prod.js gzip 49 kB 49 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.8 kB 32.8 kB
pages-turbo....dev.js gzip 52.5 kB 52.5 kB
pages-turbo...prod.js gzip 38.5 kB 38.5 kB
pages.runtim...dev.js gzip 52.5 kB 52.5 kB
pages.runtim..prod.js gzip 38.4 kB 38.4 kB
server.runti..prod.js gzip 62 kB 62 kB
Total 2.82 MB 2.82 MB ✅ -2 B
📎 Tarball URL
next@https://vercel-packages.vercel.app/next/prs/90510/next

… of throwing

Replace throw-based control flow with a return-based approach for better
performance. The function now returns `{ value: any } | null` — `null` when
the declaration is not found, `{ value }` when found. UnsupportedValueError
is still thrown for genuinely unsupported values.

Remove NoSuchDeclarationError (no longer needed) and update all callsites
in get-page-static-info.ts to unwrap the result.
…portedValueError

Replace throw-based control flow in extractValue with a discriminated
return type: `{ value: any } | { unsupported: string; path?: string }`.

- Extract path-formatting logic from UnsupportedValueError constructor
  into a standalone formatCodePath helper
- All 6 throw sites become return statements
- 3 recursive call sites propagate unsupported results
- Remove UnsupportedValueError class entirely
- Remove try/catch blocks from all 4 callsites in get-page-static-info.ts
- Update warnAboutUnsupportedValue to accept the result type directly
@sokra sokra force-pushed the sokra/avoid-throw branch from 6e366ed to 9f9107f Compare February 26, 2026 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants