Next.js Dynamic Server Error: Using searchParams
in Route Handlers
Problem Statement
When building Next.js applications (version 13+), developers may encounter an error during production builds:
[Error]: Dynamic server usage: Page couldn't be rendered statically because it used `nextUrl.searchParams`
This error typically occurs when:
- Using
nextUrl.searchParams
ornew URL(request.url)
in API route handlers - Wrapping dynamic operations in
try/catch
blocks that swallow Next.js internal errors - The code works in development (
npm run dev
) but fails during production builds (npm run build
)
The core issue arises because Next.js attempts to statically generate routes by default, but cannot do so when runtime data (like URL parameters) is accessed.
Why This Error Occurs
Next.js route handlers are static by default (until Next.js 15) meaning:
- Next.js tries to render routes at build time for performance optimization
- When runtime data (headers, URL searchParams) is detected, a
DynamicServerError
is thrown internally - This error signals Next.js to automatically switch to dynamic rendering
The critical problem happens when this internal error is caught in custom try/catch
blocks:
export const GET = async (request) => {
try {
// Dynamic usage within try block causes internal error ➡️
const params = request.nextUrl.searchParams;
// ... but we catch ALL errors below
} catch (error) {
// Custom error handling prevents Next.js from detecting dynamic usage ✖️
return NextResponse.json({ error: 'Internal error' });
}
}
By catching the DynamicServerError
, you prevent Next.js from switching to dynamic rendering mode, causing the build to fail.
Optimal Solutions
Solution 1: Move Dynamic Operations Outside try/catch
Move any dynamic data access before your try
block:
export const GET = async (request) => {
// Access dynamic data FIRST
const searchParams = request.nextUrl.searchParams;
const email = searchParams.get('email');
try {
// Proceed with your logic
const users = await getUsers({ email });
return NextResponse.json(users);
} catch (error) {
// Handle non-Next.js errors
return NextResponse.json({ error: 'Internal Server Error' });
}
};
Solution 2: Force Dynamic Rendering Explicitly
Add to your API route file:
export const dynamic = 'force-dynamic';
This approach:
- Explicitly marks the route as dynamic
- Eliminates the static generation attempt
- Works in both app and page routers
export const dynamic = 'force-dynamic'; // Add this export
export const GET = async (request) => {
const searchParams = request.nextUrl.searchParams;
// ... rest of code
};
Solution 3: Rethrow Next.js Errors (Advanced)
For situations where you must keep dynamic operations inside try/catch
:
import { isDynamicServerError } from 'next/dist/client/components/hooks-server-context';
export const GET = async (request) => {
try {
const searchParams = request.nextUrl.searchParams;
// ... dynamic operations
} catch (error) {
if (isDynamicServerError(error)) {
throw error; // Let Next.js handle its own errors
}
return NextResponse.json({ error: 'Custom error' });
}
};
Note for Next.js 15+: Use unstable_rethrow
:
import { unstable_rethrow } from 'next/dist/client/components/server-hooks';
try {
const searchParams = new URL(request.url).searchParams;
} catch (error) {
unstable_rethrow(error);
// Handle other errors below...
}
When to Use Each Approach
Approach | Best For | Caveats |
---|---|---|
External access | Simple routes | Don't use if parameter access must be in try block |
force-dynamic | Rapid fixing, API routes | Disables static optimization |
Rethrowing errors | Complex error handling | Requires Next.js version 14+ |
Best Practices for SearchParams
When working with URL parameters:
- Avoid creating URL objects excessively: Reuse a single instance if possible
- Validate parameters early: Ensure parameters meet expectations before further processing
- Use TypeScript for safety: Type coercion for URL parameters:
const id = searchParams.get('id') ? Number(searchParams.get('id')) : undefined;
- Combine the external access method with force-dynamic for critical API routes:
export const dynamic = 'force-dynamic';
export const GET = async (request: Request) => {
const { searchParams } = new URL(request.url);
// ... safe processing
};
Why Development vs. Production Differ
- Development mode: Runs in Node.js without static generation optimizations
- Production build: Attempts static optimization during
next build
- Dynamic operations require explicit handling to prevent failed builds
WARNING
Avoid this anti-pattern:
export const GET = async (request) => {
try {
// ❌ Accesses dynamic data inside try block
const url = new URL(request.url);
} catch (error) {
// ❌ Swallows Next.js dynamic detection error
return handleError(error);
}
}
By following these patterns, you resolve the "Dynamic server usage" error while maintaining both development flexibility and production optimization.