Enable Dark Mode!
how-to-handle-loading-and-error-states-in-nextjs-server-components.jpg
By: Alan Joy

How to Handle Loading and Error States in Next.js Server Components

Technical

Next.js 13+ introduced the App Router, React Server Components, and a more powerful routing model. With these improvements came a new, intuitive way to handle loading and error states directly at the route level. For beginners, this is one of the most valuable features because it eliminates boilerplate and lets you focus on building great UX.

Understanding Loading & Error States in the App Router

In traditional React, you manually handle loading and errors inside components like this:

if (isLoading) return <Spinner />
if (error) return <ErrorComponent />

But with Server Components, data is fetched before rendering the UI. That means loading and error states occur at the route level, not inside individual components.

Next.js solves this with two special files:

  • loading.js: Automatically shown while the route's Server Components are being fetched
  • error.js: Automatically displayed when the route throws an error

Both files sit next to the page and control the UX for that route.

Basic File Structure

app/
 +-- dashboard/
      +-- page.js
      +-- loading.js
      +-- error.js

When /dashboard loads:

  • loading.js appears first
  • page.js appears when data is ready
  • If something goes wrong, error.js appears

1. Handling Loading States with loading.js

When your Server Component is waiting for data (database, API, CMS, etc.), Next.js automatically shows loading.js.

Create a loading.js file:

// app/dashboard/loading.js
export default function Loading() {
  return (
    <div className="flex items-center justify-center h-[60vh]">
      <p className="text-lg font-medium">Loading dashboard...</p>
    </div>
  );
}

Example Server Component with async fetch

// app/dashboard/page.js
export default async function DashboardPage() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users", {
    cache: "no-store",
  });
  const users = await response.json();
  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id} className="py-1 border-b">
            {user.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

When this page starts loading, loading.js displays.

2. Handling Error States with error.js

If your Server Component throws an error (network failure, DB issue, code bug), Next.js will automatically render error.js.

Create an error.js file:

// app/dashboard/error.js
"use client";
export default function Error({ error, reset }) {
  return (
    <div className="p-6">
      <h2 className="text-xl font-semibold text-red-600">
        Something went wrong!
      </h2>
      <p className="mt-2 text-gray-700">{error.message}</p>
      <button
        onClick={reset}
        className="mt-4 bg-blue-600 text-white px-4 py-2 rounded"
      >
        Try Again
      </button>
    </div>
  );
}

reset() re-renders the Server Component, useful when fetching external data. Make sure to add "use client", error.js must be a Client Component because it handles button clicks.

3. Throwing Errors Manually in Server Components

You can throw errors directly inside your Server Component:

// app/dashboard/page.js
export default async function DashboardPage() {
  const res = await fetch("https://api.example.com/data");
  if (!res.ok) {
    throw new Error("Failed to fetch dashboard data");
  }
  const data = await res.json();
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

The above error automatically shows your route’s error.js.

4. Using Suspense for Component-Level Loading

If you want loading states inside your page, not just route level. Next.js supports React Suspense even for Server Components.

Example

// app/dashboard/page.js
import { Suspense } from "react";
import UserList from "./user-list";
export default function Dashboard() {
  return (
    <Suspense fallback={<p>Loading users...</p>}>
      <UserList />
    </Suspense>
  );
}

user-list.js (Server Component)

// app/dashboard/user-list.js
export default async function UserList() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = await res.json();
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

5. Best Practices for Production

  • Keep loading.js lightweight
  • Avoid heavy animations or expensive computations. Users should see it instantly.

  • Do not expose sensitive error messages
  • In production, throw generic errors:

throw new Error("Unable to load user data");
  • Use cache: "no-store" for dynamic data
  • Prevents stale responses.

  • Prefer Suspense for partially loading pages
  • Pages feel faster when some sections load independently.

  • Log server errors
  • You can integrate logging:

if (!res.ok) {
  console.error("API error:", res.statusText);
  throw new Error("Failed to fetch data");
}
  • Consider global error boundaries
  • Add a root-level error.js inside app/ to catch everything.

Conclusion

Next.js Server Components make loading and error handling cleaner than ever. Instead of juggling state variables inside components, you let Next.js manage it at the routing level using loading.js and error.js.

To read more about A Complete Beginner’s Guide to Getting Started with Next.js, refer to our blog A Complete Beginner’s Guide to Getting Started with Next.js.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



Recent Posts

whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message