Back

Best Practices for Error Logging in JavaScript

Best Practices for Error Logging in JavaScript

Production JavaScript applications fail silently every day. Users encounter errors that developers never see, leading to poor experiences and lost revenue. The difference between applications that catch these issues and those that don’t? Proper error logging.

This article covers essential practices for implementing robust JavaScript error logging across frontend and backend environments. You’ll learn how to move beyond console.log, implement structured logging with proven frameworks, and build a system that captures critical errors before users report them.

Key Takeaways

  • Console logging lacks persistence, centralization, and structure needed for production environments
  • Structured logging with frameworks like Winston or Pino provides machine-parsable data for analysis
  • Frontend error handling requires global handlers and framework-specific solutions like React Error Boundaries
  • Protecting sensitive data and including contextual information are critical for effective logging

Why Console Logging Falls Short in Production

Most developers start with console.log() for debugging. While adequate during development, this approach fails in production:

// This error disappears into the user's browser
try {
  processPayment(order);
} catch (error) {
  console.error(error); // Lost forever in production
}

Console methods lack:

  • Persistence beyond the current session
  • Centralized collection across users
  • Structured data for analysis
  • Severity levels for prioritization
  • Sensitive data protection

Production applications need logging that captures, structures, and transmits errors to a central location for analysis.

Implementing Structured Logging with Proven Frameworks

Choosing the Right Framework

For Node.js logging, two frameworks dominate the ecosystem:

Winston offers flexibility and extensive transport options:

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' })
  ]
});

Pino prioritizes performance with minimal overhead:

const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
  formatters: {
    level: (label) => ({ level: label })
  }
});

Structuring Your Logs

Replace unstructured strings with JSON objects that machines can parse and analyze:

// Bad: Unstructured string
logger.info(`User ${userId} failed login attempt`);

// Good: Structured JSON
logger.info({
  event: 'login_failed',
  userId: userId,
  ip: request.ip,
  timestamp: new Date().toISOString(),
  userAgent: request.headers['user-agent']
});

Essential Components of Effective JavaScript Error Logging

1. Use Appropriate Log Levels

Implement consistent severity levels across your application:

logger.debug('Detailed debugging information');
logger.info('Normal application flow');
logger.warn('Warning: degraded performance detected');
logger.error('Error occurred but application continues');
logger.fatal('Critical failure, application shutting down');

2. Always Include Stack Traces

Capture the full error context for debugging:

process.on('uncaughtException', (error) => {
  logger.fatal({
    message: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString()
  });
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error({
    message: 'Unhandled Promise Rejection',
    reason: reason,
    promise: promise
  });
});

3. Add Contextual Information

Include request IDs, user IDs, and session data to trace issues:

const requestLogger = logger.child({
  requestId: generateRequestId(),
  sessionId: request.session.id
});

requestLogger.info('Processing payment request');

4. Protect Sensitive Data

Never log passwords, tokens, or personal information:

const logger = pino({
  redact: ['password', 'creditCard', 'ssn', 'authorization']
});

// These fields will be automatically redacted
logger.info({
  user: email,
  password: 'secret123', // Will show as [REDACTED]
  action: 'login_attempt'
});

Frontend Error Handling Strategies

Global Error Handlers

Capture all unhandled errors in browser environments:

window.addEventListener('error', (event) => {
  logToServer({
    message: event.message,
    source: event.filename,
    line: event.lineno,
    column: event.colno,
    stack: event.error?.stack
  });
});

window.addEventListener('unhandledrejection', (event) => {
  logToServer({
    type: 'unhandledRejection',
    reason: event.reason,
    promise: event.promise
  });
});

React Error Boundaries

For React applications, implement error boundaries to catch component errors:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logger.error({
      message: error.toString(),
      componentStack: errorInfo.componentStack,
      timestamp: new Date().toISOString()
    });
  }

  render() {
    if (this.state.hasError) {
      return <h2>Something went wrong. Please refresh the page.</h2>;
    }
    return this.props.children;
  }
}

Centralizing Logs for Analysis

Direct all logs to stdout and let your infrastructure handle routing:

// Configure logger to output to stdout only
const logger = pino({
  transport: {
    target: 'pino-pretty',
    options: {
      destination: 1 // stdout
    }
  }
});

This approach allows Docker, Kubernetes, or log shippers like Fluentd to collect and route logs to centralized systems for analysis. For client-side applications, implement a simple endpoint to receive and forward logs from browsers to your centralized logging infrastructure.

Conclusion

Effective JavaScript error logging requires more than replacing console.log with a framework. It demands structured data, appropriate severity levels, comprehensive error context, and centralized collection. By implementing these practices with frameworks like Winston or Pino, protecting sensitive data, and establishing proper error boundaries in frontend code, you create a system that catches issues before they impact users. Start with these fundamentals, then expand based on your application’s specific monitoring needs.

FAQs

Pino has minimal overhead, adding only 2-3% latency in most cases. Winston is slightly heavier but still negligible for most applications. Both are production-ready and used by high-traffic applications worldwide.

Use built-in redaction features in your logging framework to automatically mask sensitive fields. Define a list of field names to redact like passwords, tokens, and credit card numbers. Always audit your logs regularly for accidental data exposure.

Log errors on both sides. Client-side logging captures browser-specific issues and JavaScript errors that never reach your server. Server-side logging handles API errors and backend failures. Use a centralized system to aggregate both sources.

Configure alerts for error and fatal levels immediately. Warning levels can trigger daily summaries. Info and debug levels should be searchable but not trigger alerts unless you're investigating specific issues or monitoring critical business events.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay