Next.js Error Tracking Without the Bloat
Tutorials October 6, 2025 ยท 3 min read

Next.js Error Tracking Without the Bloat

Set up lightweight error tracking for Next.js without heavy monitoring SDKs. Capture client and server errors with a simple setup.

Many Next.js error tracking solutions require heavy SDKs that bloat your bundle size and send data to expensive monitoring platforms. But you don't need all that. Here's how to set up lightweight error tracking that captures what matters.

The Problem with Heavy Solutions

Popular error tracking tools like Sentry add significant bundle size and complexity:

  • SDK size: 30-100KB+ added to your client bundle
  • Configuration complexity: Multiple integrations to set up
  • Pricing: Can quickly become expensive at scale
  • Data overload: Captures more than you need

Lightweight Alternative

We'll build error tracking that:

  • Adds minimal bundle size (<2KB)
  • Captures both client and server errors
  • Sends to a simple logging endpoint
  • Includes essential context

Client-Side Error Tracking

Create a simple error boundary and global error handler:

// lib/logger.ts
class Logger {
  private buffer: any[] = [];
  private flushTimeout: NodeJS.Timeout | null = null;

  log(level: string, message: string, context: Record<string, any> = {}) {
    this.buffer.push({
      level,
      message,
      timestamp: new Date().toISOString(),
      context: {
        ...context,
        url: typeof window !== 'undefined' ? window.location.href : undefined,
        userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
      },
    });

    if (this.buffer.length >= 5) {
      this.flush();
    } else if (!this.flushTimeout) {
      this.flushTimeout = setTimeout(() => this.flush(), 5000);
    }
  }

  private async flush() {
    if (this.buffer.length === 0) return;

    const logs = [...this.buffer];
    this.buffer = [];

    if (this.flushTimeout) {
      clearTimeout(this.flushTimeout);
      this.flushTimeout = null;
    }

    try {
      await fetch('/api/logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ logs }),
      });
    } catch (e) {
      console.error('Failed to send logs', e);
    }
  }

  error(message: string, context?: Record<string, any>) {
    this.log('error', message, context);
  }

  info(message: string, context?: Record<string, any>) {
    this.log('info', message, context);
  }
}

export const logger = new Logger();

Global Error Handlers

// app/error-handlers.tsx
'use client';

import { useEffect } from 'react';
import { logger } from '@/lib/logger';

export function ErrorHandlers() {
  useEffect(() => {
    // Catch unhandled errors
    const handleError = (event: ErrorEvent) => {
      logger.error(event.message, {
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack,
      });
    };

    // Catch unhandled promise rejections
    const handleRejection = (event: PromiseRejectionEvent) => {
      logger.error('Unhandled Promise Rejection', {
        reason: event.reason?.message || String(event.reason),
        stack: event.reason?.stack,
      });
    };

    window.addEventListener('error', handleError);
    window.addEventListener('unhandledrejection', handleRejection);

    return () => {
      window.removeEventListener('error', handleError);
      window.removeEventListener('unhandledrejection', handleRejection);
    };
  }, []);

  return null;
}

API Route for Logging

// app/api/logs/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { logs } = await request.json();

  // Forward to your logging service
  await fetch('https://logs.401clicks.com/api/v1/logs/batch', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.CLICKS_API_TOKEN}`,
    },
    body: JSON.stringify({ logs }),
  });

  return NextResponse.json({ ok: true });
}

Server-Side Error Handling

// app/error.tsx
'use client';

import { useEffect } from 'react';
import { logger } from '@/lib/logger';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    logger.error(error.message, {
      digest: error.digest,
      stack: error.stack,
    });
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Add to Layout

// app/layout.tsx
import { ErrorHandlers } from './error-handlers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ErrorHandlers />
        {children}
      </body>
    </html>
  );
}

Benefits

  • Tiny footprint: ~2KB vs 50KB+ for Sentry
  • Full control: You decide what gets logged
  • Cost effective: Use any logging backend
  • Privacy friendly: No third-party data collection

Conclusion

You don't need a heavy monitoring solution for effective error tracking. This lightweight approach captures the errors that matter without the bloat, giving you full control over your logging infrastructure.

A

Admin

Published on October 6, 2025