Errors & Logs
Define consistent patterns for handling, reporting, and logging errors across the application.
Proper error handling improves debuggability, user experience, and system reliability, while secure and structured logging aids in monitoring and incident response.
Don’t swallow errors silently
Always handle or log errors when using try/catch, promise chains, or callbacks. Silent failures make debugging difficult and can mask serious bugs or broken flows.
1try {
2 await sendEmail();
3} catch (e) {
4 // do nothing
5}
1try {
2 await sendEmail();
3} catch (e) {
4 logger.error("Failed to send email:", e);
5 throw e; // or handle it gracefully
6}
Use consistent error structures
Use a standardized structure for application errors. This makes it easier to handle errors uniformly in middleware, clients, and logs.
throw new AppError("User not found", 404, { userId });
Where AppError
might extend Error
and include fields like statusCode
, context
, and isOperational
.
Separate operational and programmer errors
Operational errors (e.g., invalid input, DB timeout) are expected and should be handled gracefully. Programmer errors (e.g., undefined is not a function
) are bugs and should not be caught silently.
Centralize error handling
Use a centralized error handling mechanism (e.g., Express middleware, global fallback in backend apps) to format responses, log errors, and avoid repetition across routes or services.
1app.use((err, req, res, next) => {
2 logger.error(err);
3 const status = err.statusCode || 500;
4 res.status(status).json({ error: err.message });
5});
Always set correct HTTP status codes
Respond with appropriate HTTP status codes to reflect the type of error:
res.status(200).json({ error: "Invalid credentials" });
res.status(401).json({ error: "Invalid credentials" });
Don’t log sensitive information in production
Avoid logging sensitive data like passwords, tokens, or PII. Mask values or redact fields before logging, especially in error traces that get reported to monitoring tools or stored long-term.
logger.warn("Login failed for", { email, password });
logger.warn("Login failed for user", { email });
Use proper log levels
Log messages with meaningful severity levels to help filter and prioritize issues:
1logger.info("User registered", { userId });
2logger.warn("Payment response took too long");
3logger.error("Payment failed", { error });
Include context in error logs
Include relevant metadata (e.g., user ID, request ID, resource name) with each error or warning log. This makes debugging easier and supports tracing across services.
1logger.error("Order processing failed", {
2 orderId,
3 userId,
4 error: err.message,
5});
Show user-friendly error messages
Never expose raw error stacks or internal details to the user. Return clear, generic error messages while logging the full technical details for developers.
1{
2 "error": "TypeError: Cannot read property 'email' of undefined"
3}
1{
2 "error": "Something went wrong. Please try again later."
3}
Retry transient failures when safe
For transient errors (e.g. network issues, rate limits), implement retry logic with backoff. But never blindly retry on all errors — some may be non-recoverable or unsafe to repeat.
1retry(fetchData, {
2 retries: 3,
3 delay: (attempt) => 1000 * 2 ** attempt,
4});
Provide fallbacks where appropriate
Gracefully degrade functionality by using cached data, default responses, or placeholder components when non-critical errors occur. This improves resilience and user experience.
1try {
2 const settings = await fetchUserSettings();
3 applySettings(settings);
4} catch {
5 applyDefaultSettings(); // fallback
6}
Integrate alerting for critical errors
Set up alerting for critical application errors (e.g., service crashes, failed logins spike, DB outages). Use tools like Sentry, Datadog, or Grafana to monitor and respond to production issues in real time.