Always log errors with structured context: user ID, request ID, input data, stack trace. An error message like "Cannot read property of undefined" with no context is impossible to debug — you don't know which user hit it, what they were doing, or how to reproduce it.
Error logs without context are useless for debugging. "Cannot read property of undefined" without knowing which user, which request, which input, and which code path is impossible to reproduce or fix. Developers waste hours guessing instead of minutes reading structured logs with full context.
BeforeMerge scans your pull requests against this rule and 3+ others. Get actionable feedback before code ships.
An error log that says TypeError: Cannot read properties of undefined (reading 'name') tells you almost nothing. You do not know:
Without this context, debugging is archaeology: you read the stack trace, guess at what data could cause it, search logs for related events, and try to reconstruct the scenario. This turns a 5-minute fix into a multi-hour investigation.
Structured error logs with context make every error immediately actionable. You see the user, the input, the code path, and the state — and you can often reproduce and fix the issue in minutes.
Every error log must include: (1) a human-readable message, (2) the original error/stack trace, (3) structured context (user ID, request ID, input data, operation name). Use structured logging (JSON) so logs are searchable and filterable.
// Useless error logs
try {
await processOrder(order);
} catch (error) {
console.log("Error"); // No details at all
console.error(error); // Stack trace but no context
console.log("Failed to process order"); // What order? Which user?
}import { logger } from "@/lib/logger";
try {
await processOrder(order);
} catch (error) {
logger.error("Failed to process order", {
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
name: error.name,
} : String(error),
context: {
userId: auth.userId,
orderId: order.id,
orderTotal: order.total,
paymentMethod: order.paymentMethod,
requestId: req.headers["x-request-id"],
},
});
// Re-throw or return appropriate error to caller
throw new AppError("ORDER_PROCESSING_FAILED", {
orderId: order.id,
cause: error,
});
}// lib/logger.ts — structured JSON logger
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL || "info",
formatters: {
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});Search for bare console.error and console.log in catch blocks:
grep -rn 'console\.log\|console\.error' --include="*.ts" --include="*.tsx" | grep -v node_modulesAny console.error(error) without additional context is a finding.
pino, winston, or tsloglogger instance with consistent configurationconsole.error(error) calls with logger.error(message, { error, context })Sentry.setUser({ id: userId })