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.
Admin
Published on October 6, 2025