Error Security
Rules
- Never leak stack traces, database errors, or file paths to clients in production
- Return generic error messages with a unique error ID:
{ error: "Something went wrong", errorId: "abc123" }
- Log full error details server-side with the same correlation ID for debugging
- Use different error detail levels: development (full stack trace) vs production (safe message + ID)
- Catch all unhandled errors with global error handlers — never let raw errors reach the client
- Sanitize error messages from third-party services before forwarding to clients
- Return proper HTTP status codes: 400 (bad input), 401 (unauthenticated), 403 (forbidden), 404 (not found), 500 (server error)
import { randomUUID } from "node:crypto";
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const errorId = randomUUID();
console.error({ errorId, message: err.message, stack: err.stack, url: req.url });
const statusCode = (err as any).statusCode || 500;
res.status(statusCode).json({
error: statusCode >= 500 ? "Internal server error" : err.message,
errorId,
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
});
});
export default function GlobalError({ error, reset }) {
return (
<div>
<h2>Something went wrong</h2>
<p>Error ID: {error.digest}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Avoid
- Returning
err.message directly — database errors leak table names, column names, and query structure
- Stack traces in production responses — they reveal file paths, dependencies, and internal architecture
- Generic 500 for everything — use proper status codes so clients can handle errors appropriately
- Logging errors without correlation IDs — makes production debugging nearly impossible
- Swallowing errors silently — always log, even if the response is generic