| name | error-handling-security |
| description | Prevent information leakage through error messages. Covers sanitized error responses, structured error handling, no stack traces in production, and safe logging patterns for Express, Next.js, Cloudflare Workers, Laravel, and FastAPI. Use when fixing verbose errors, implementing error handlers, or preventing stack trace leaks. |
| version | 1.0 |
Error Handling Security
Prevent stack traces, database errors, and internal details from leaking to attackers. Verbose errors in production are a reconnaissance goldmine -- they reveal your framework, database structure, file paths, and dependencies.
The Rule
NEVER expose internal error details to users. ALWAYS return generic messages externally and log details internally.
What Leaks Look Like
BAD -- Exposes everything to attacker:
{
"error": "ER_NO_SUCH_TABLE: Table 'myapp_prod.users_v2' doesn't exist",
"stack": "Error: ER_NO_SUCH_TABLE\n at /app/src/services/user.ts:47:12\n at Pool.query (/app/node_modules/mysql2/promise.js:94:22)",
"sql": "SELECT * FROM users_v2 WHERE email = 'admin@test.com'"
}
This tells the attacker: database type (MySQL), table names, file structure, dependencies, and the exact query being run.
GOOD -- Generic external, detailed internal:
{
"error": "Something went wrong",
"code": "INTERNAL_ERROR",
"requestId": "req_abc123"
}
Meanwhile, the full error is logged server-side with the requestId for debugging.
Framework Implementations
Cloudflare Workers
interface AppError {
code: string;
message: string;
internal?: string;
status: number;
}
function handleError(error: unknown, requestId: string): Response {
console.error(JSON.stringify({
requestId,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
}));
if (error instanceof AppError) {
return Response.json(
{ error: error.message, code: error.code, requestId },
{ status: error.status }
);
}
return Response.json(
{ error: 'Internal server error', code: 'INTERNAL_ERROR', requestId },
{ status: 500 }
);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const requestId = crypto.randomUUID();
try {
return await handleRequest(request, env);
} catch (error) {
return handleError(error, requestId);
}
}
};
Express.js
BAD:
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message, stack: err.stack });
});
GOOD:
class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public internal?: string
) {
super(message);
}
}
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
console.error(JSON.stringify({
requestId,
method: req.method,
path: req.path,
error: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
}));
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
code: err.code,
requestId,
});
}
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR',
requestId,
});
});
app.get('/users/:id', async (req, res, next) => {
try {
const user = await getUser(req.params.id);
if (!user) throw new AppError(404, 'NOT_FOUND', 'User not found');
res.json(user);
} catch (err) {
next(err);
}
});
Next.js
export async function GET(request: Request) {
try {
const users = await getUsers();
return Response.json(users);
} catch (error) {
console.error('[API /users]', error);
return Response.json(
{ error: 'Failed to fetch users', code: 'FETCH_ERROR' },
{ status: 500 }
);
}
}
Laravel
class Handler extends ExceptionHandler
{
protected $dontReport = [
AuthenticationException::class,
ValidationException::class,
];
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
$requestId = $request->header('X-Request-ID', Str::uuid());
Log::error('API Error', [
'request_id' => $requestId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'url' => $request->fullUrl(),
]);
if ($e instanceof ModelNotFoundException) {
return response()->json([
'error' => 'Resource not found',
'code' => 'NOT_FOUND',
'request_id' => $requestId,
], 404);
}
return response()->json([
'error' => 'Internal server error',
'code' => 'INTERNAL_ERROR',
'request_id' => $requestId,
], 500);
}
return parent::render($request, $e);
}
}
Python FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging, uuid
app = FastAPI()
logger = logging.getLogger(__name__)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
request_id = str(uuid.uuid4())
logger.error(f"[{request_id}] {request.method} {request.url} - {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": "Internal server error",
"code": "INTERNAL_ERROR",
"request_id": request_id,
},
)
Environment Configuration
NODE_ENV=production
APP_DEBUG=false
LOG_LEVEL=error
Error Handling Checklist
PDPA 2024 Relevance
Verbose error messages that reveal database structure, user data, or system internals constitute an information disclosure vulnerability. Under PDPA 2024, this aids attackers in exploiting the system to access personal data -- making the developer liable for failing to implement reasonable security measures under Seksyen 5(1).