Upgrading to Next.js 15 brings major architectural refinements designed to optimize streaming performance and developer predictability. However, two fundamental breaking changes catch engineering teams off-guard: dynamic request APIs (such as params, searchParams, cookies(), and headers()) transitioning from synchronous objects to Promises, and the complete paradigm shift from "cached-by-default" to "uncached-by-default".
This guide breaks down the core runtime errors triggered by these changes and provides production-ready refactoring patterns to ensure a seamless upgrade.
Key Paradigm Shifts in Next.js 15
- Async Dynamic Request APIs: In Next.js 14 and earlier, request-time data like route parameters were available synchronously. In Next.js 15, accessing route segments or request context asynchronously allows Next.js to unblock layout rendering earlier and optimize server streaming.
- Opt-in Caching Model:
fetch()GET requests, GET Route Handlers, and Client Router Cache are no longer cached by default. You must explicitly define your caching intentions using options likecache: 'force-cache'or dynamic configuration flags.
1. Fixing the Async Params Breaking Change
If you access properties directly on params or searchParams without unwrapping the Promise, Next.js 15 will throw runtime warnings or fail during build execution.
Server Component Pattern
// ❌ BEFORE (Next.js 14): Synchronous params access
export default function ProductPage({ params }: { params: { id: string } }) {
const productId = params.id;
return <div>Product ID: {productId}</div>;
}
// ✅ AFTER (Next.js 15): Await params as a Promise
type PageProps = {
params: Promise<{ id: string }>;
searchParams: Promise<{ query?: string }>;
};
export default async function ProductPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { query } = await searchParams;
return (
<div>
<h1>Product ID: {id}</h1>
<p>Search Query: {query ?? 'None'}</p>
</div>
);
}
Client Component Pattern using React use() Hook
In Client Components, props passed to the component must be unwrapped using React 19's use() API instead of await:
'use client';
import { use } from 'react';
type ClientProps = {
params: Promise<{ slug: string }>;
};
export default function ArticleClientComponent({ params }: ClientProps) {
// ✅ Resolves the Promise natively inside React client tree
const { slug } = use(params);
return <article>Active Slug: {slug}</article>;
}
2. Managing Opt-in Caching Semantics
Because fetch() requests in Next.js 15 default to no-store behavior, your server components will perform fresh network calls on every request unless configured otherwise.
Problem: Unintended Dynamic Data Refetching
// ❌ NEXT.JS 15 DEFAULT: Uncached on every request
async function getGlobalSettings() {
// Makes an HTTP request on EVERY page render
const res = await fetch('https://api.example.com/settings');
return res.json();
}
Solution: Explicit Opt-in Strategy
// ✅ OPTION A: Persistent Data Cache (Static / SSG behavior)
async function getGlobalSettings() {
const res = await fetch('https://api.example.com/settings', {
cache: 'force-cache', // Force caching across requests
});
return res.json();
}
// ✅ OPTION B: Time-based Revalidation (ISR behavior)
async function getProductCatalog() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // Cache valid for 1 hour
});
return res.json();
}
Production Refactoring Checklist
- Run Codemod Automation: Execute
npx @next/codemod@latest upgrade latestto automatically update synchronousparams,cookies(), andheaders()usages across your codebase. - Audit Data Fetching Operations: Check external API calls. If static pages began hitting backend databases continuously after upgrading, apply explicit
cache: 'force-cache'or route segment configs. - Update Route Handlers: Audit GET route handlers in
app/api/...routes. To cache GET endpoints, addexport const dynamic = 'force-static';explicitly.