React Logging and Error Handling Best Practices
In this guide, we'll explore battle-tested patterns for implementing production-ready logging and error handling in React applications using logzai. Whether you're building a small side project or maintaining a large-scale application, these practices will help you catch bugs faster, understand user behavior better, and ship more reliable software.
Understanding the Foundation
Why Logging Matters in React
Client-side logging presents unique challenges compared to server-side logging:
- Distributed Environments: Your app runs in thousands of different browsers, devices, and network conditions
- Limited Visibility: Without proper instrumentation, you only see errors that get reported—most issues go unnoticed
- User Context: Understanding what the user was doing when an error occurred is critical for debugging
- Performance Impact: Excessive logging can slow down your app and frustrate users
The key is finding the right balance: log enough to debug effectively, but not so much that you impact performance or create noise.
The Cost of Poor Error Handling
When error handling is an afterthought, the consequences ripple through your entire organization:
- Lost Revenue: Users who encounter errors often abandon their workflows—shopping carts left behind, forms never submitted
- Support Burden: Your support team spends hours trying to reproduce issues with incomplete information
- Development Time: Engineers waste days tracking down bugs that could have been caught in minutes with proper logging
- User Trust: Frequent errors erode confidence in your product, making users less likely to return
What Makes Good Logging
Good logging isn't about quantity—it's about quality and structure:
- Structured Data: Logs should be machine-readable with consistent fields (JSON, not strings)
- Appropriate Levels: Use debug, info, warn, and error levels correctly
- Rich Context: Include user IDs, session IDs, routes, and relevant state
- Actionable Information: Every log should help you understand what happened and why
- Performance-Aware: Logging should never block your UI thread
With these principles in mind, let's dive into implementation.
Setting Up LogzAI in Your React Application
Getting started with logzai is straightforward. First, install the package:
1npm install logzai-js 2# or 3pnpm add logzai-js 4# or 5yarn add logzai-js
Next, initialize logzai in your application entry point (typically main.tsx or index.tsx):
1import logzai from 'logzai-js' 2import { browserPlugin } from 'logzai-js/browser' 3 4// Initialize logzai before rendering your app 5logzai.init({ 6 ingestToken: 'your-ingest-token', 7 ingestEndpoint: 'https://ingest.logzai.com', 8 serviceName: 'my-react-app', 9 environment: process.env.NODE_ENV, // 'development' or 'production' 10 mirrorToConsole: process.env.NODE_ENV === 'development', // See logs in console during dev 11}) 12 13// Enable the browser plugin for automatic error handling 14logzai.plugin('browser', browserPlugin)
What the Browser Plugin Does Automatically
The browser plugin is a powerful addition that transforms your error handling with zero additional code. Once enabled, it automatically:
- Captures all JavaScript errors: Hooks into
window.onerrorto catch unhandled exceptions - Captures unhandled promise rejections: Monitors
window.onunhandledrejectionto catch async errors that slip through - Logs errors with full context: Automatically includes stack traces, error messages, and browser information
- Non-blocking operation: Sends logs asynchronously without impacting your app's performance
This means even errors you didn't explicitly catch will be logged to logzai, giving you complete visibility into production issues.
Customizing the Browser Plugin
The browser plugin accepts configuration options to enhance your logs with application-specific context:
1import logzai from 'logzai-js' 2import { browserPlugin } from 'logzai-js/browser' 3import { store } from './store' 4import { selectCurrentUser, selectSelectedOrg } from './store/selectors' 5 6// Initialize logzai 7logzai.init({ 8 ingestToken: 'your-ingest-token', 9 ingestEndpoint: 'https://ingest.logzai.com', 10 serviceName: 'my-react-app', 11 environment: process.env.NODE_ENV, 12 mirrorToConsole: process.env.NODE_ENV === 'development', 13}) 14 15// Configure browser plugin with custom options 16logzai.plugin('browser', browserPlugin, { 17 // Custom message formatter for errors 18 messageFormatter: (error: any) => { 19 return `Exception: ${error.message}` 20 }, 21 22 // Context injector - automatically adds context to every log and error 23 contextInjector: () => { 24 const state = store.getState() 25 const currentUser = selectCurrentUser(state) 26 const selectedOrg = selectSelectedOrg(state) 27 28 return { 29 userId: currentUser?.id, 30 userEmail: currentUser?.email, 31 orgId: selectedOrg?.id, 32 orgName: selectedOrg?.name, 33 currentRoute: window.location.pathname, 34 userAgent: navigator.userAgent, 35 viewport: `${window.innerWidth}x${window.innerHeight}`, 36 } 37 }, 38 39 // Optional: Filter out errors you don't want to log 40 errorFilter: (error: Error) => { 41 // Return false to skip logging this error 42 if (error.message.includes('ResizeObserver')) { 43 return false // Don't log benign ResizeObserver errors 44 } 45 return true // Log all other errors 46 }, 47})
Key Options:
- messageFormatter: Transform how error messages appear in logs
- contextInjector: Inject application state (user, org, route) into every log automatically
- errorFilter: Skip logging specific errors that aren't actionable
With this setup, every error—even those you didn't anticipate—will be automatically logged with rich context about the user, their session, and the state of your application.
That's it! You're now ready to start logging with comprehensive automatic error tracking.
Logging Best Practices
1. Use Appropriate Log Levels
Understanding when to use each log level is crucial for creating a signal-to-noise ratio that actually helps you debug:
DEBUG: Use for detailed information useful during development. These logs are typically verbose and not needed in production.
1// Good use of debug 2const handleSearch = (query: string) => { 3 logzai.debug('Search initiated', { 4 query, 5 timestamp: Date.now(), 6 resultsCount: results.length, 7 }) 8}
INFO: Use for important business events and normal operations you want to track.
1// Good use of info 2const handleCheckout = async (cartItems: CartItem[]) => { 3 logzai.info('Checkout started', { 4 itemCount: cartItems.length, 5 totalValue: calculateTotal(cartItems), 6 userId: currentUser.id, 7 }) 8 9 // ... checkout logic 10}
WARN: Use for recoverable issues or deprecated features that should be addressed but don't break functionality.
1// Good use of warn 2const fetchUserData = async () => { 3 const cachedData = getFromCache('userData') 4 5 if (!cachedData) { 6 logzai.warn('Cache miss for user data', { 7 userId: currentUser.id, 8 cacheExpiry: getCacheExpiry(), 9 }) 10 return fetchFromAPI() 11 } 12 13 return cachedData 14}
ERROR: Use for unrecoverable errors that impact functionality.
1// Good use of error 2const saveUserSettings = async (settings: Settings) => { 3 try { 4 await api.updateSettings(settings) 5 } catch (error) { 6 logzai.error('Failed to save user settings', { 7 error: error.message, 8 userId: currentUser.id, 9 settings, 10 }) 11 throw error // Re-throw to let UI handle it 12 } 13}
2. Structure Your Logs with Context
Context transforms logs from cryptic messages into actionable insights. Compare these two approaches:
1// ❌ Bad: Minimal context 2logzai.info('User logged in') 3 4// ✅ Good: Rich context 5logzai.info('User logged in', { 6 userId: user.id, 7 email: user.email, 8 loginMethod: 'oauth', 9 provider: 'google', 10 timestamp: Date.now(), 11 previousLoginAt: user.lastLoginAt, 12 daysSinceLastLogin: calculateDaysSince(user.lastLoginAt), 13})
The second example gives you everything you need to understand user behavior patterns, detect anomalies, and debug issues.
Key context fields to include:
- User identifiers: userId, email, sessionId
- Business context: orderId, transactionId, itemId
- Technical context: route, component name, action type
- Temporal context: timestamps, durations, retry counts
- Environmental context: browser, device, network status
3. Context Injection Pattern
As we saw in the setup section, the browser plugin's contextInjector automatically enriches all logs and errors with application context. This pattern ensures you never have to manually add context to individual log statements.
When you configure the browser plugin with a contextInjector, every log call automatically includes that context:
1// Simple log call 2logzai.info('Feature flag toggled', { 3 featureName: 'dark-mode', 4 enabled: true 5}) 6 7// Automatically enriched with context from the browser plugin: 8// - userId, userEmail (from Redux state) 9// - orgId, orgName (from Redux state) 10// - currentRoute (from window.location) 11// - userAgent, viewport (from browser)
This pattern has several benefits:
- Consistency: Every log has the same baseline context
- DRY Principle: Write context logic once, not in every log statement
- Error Context: Even automatically caught errors include this context
- Zero Overhead: Context is injected at log time, not computed unnecessarily
The contextInjector is called for every log, so you can include dynamic information like the current route or selected organization that changes during the user's session.
4. Log User Actions and State Changes
Logging user interactions creates a breadcrumb trail that's invaluable for debugging:
1// Component with action logging 2const ProductPage = () => { 3 const handleAddToCart = (product: Product) => { 4 logzai.info('Product added to cart', { 5 productId: product.id, 6 productName: product.name, 7 price: product.price, 8 quantity: 1, 9 source: 'product-page', 10 }) 11 12 dispatch(addToCart(product)) 13 } 14 15 const handleQuickView = (product: Product) => { 16 logzai.debug('Quick view opened', { 17 productId: product.id, 18 trigger: 'hover', 19 }) 20 21 setQuickViewProduct(product) 22 } 23 24 return ( 25 // ... component JSX 26 ) 27}
For forms, log both submission and validation failures:
1const ContactForm = () => { 2 const handleSubmit = async (values: FormValues) => { 3 // Validate 4 const errors = validateForm(values) 5 6 if (Object.keys(errors).length > 0) { 7 logzai.warn('Form validation failed', { 8 formName: 'contact', 9 errors: Object.keys(errors), 10 attemptNumber: submitAttempts + 1, 11 }) 12 return 13 } 14 15 try { 16 await api.submitContactForm(values) 17 18 logzai.info('Contact form submitted successfully', { 19 formName: 'contact', 20 fieldsFilled: Object.keys(values), 21 }) 22 } catch (error) { 23 logzai.error('Contact form submission failed', { 24 formName: 'contact', 25 error: error.message, 26 }) 27 } 28 } 29 30 return ( 31 // ... form JSX 32 ) 33}
5. Performance Considerations
Logging should never slow down your app. Follow these guidelines:
Async by Default: logzai sends logs asynchronously, but avoid doing heavy computation in log statements:
1// ❌ Bad: Expensive operation in log statement 2logzai.debug('Component rendered', { 3 largeArray: expensiveComputation(data), // Blocks UI 4}) 5 6// ✅ Good: Only log what's necessary 7logzai.debug('Component rendered', { 8 dataSize: data.length, 9})
Sampling in Production: For high-frequency events, use sampling to reduce volume:
1const logScrollEvent = () => { 2 // Only log 1% of scroll events 3 if (Math.random() < 0.01) { 4 logzai.debug('Page scrolled', { 5 scrollPosition: window.scrollY, 6 scrollDepth: calculateScrollDepth(), 7 }) 8 } 9}
Environment-Specific Verbosity: Use different log levels for development vs. production:
1const isDevelopment = process.env.NODE_ENV === 'development' 2 3const logComponentMount = (componentName: string) => { 4 if (isDevelopment) { 5 logzai.debug(`Component mounted: ${componentName}`) 6 } 7}
Error Handling Best Practices
1. Implement Error Boundaries
React Error Boundaries are your first line of defense against unhandled errors crashing your entire app. They catch errors in the component tree and allow you to log them and show fallback UI:
1import React, { Component, ErrorInfo, ReactNode } from 'react' 2import logzai from 'logzai-js/browser' 3 4interface Props { 5 children: ReactNode 6 fallback?: ReactNode 7} 8 9interface State { 10 hasError: boolean 11 error?: Error 12} 13 14class ErrorBoundary extends Component<Props, State> { 15 constructor(props: Props) { 16 super(props) 17 this.state = { hasError: false } 18 } 19 20 static getDerivedStateFromError(error: Error): State { 21 return { hasError: true, error } 22 } 23 24 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 25 // Log to logzai with full context 26 logzai.exception('React error boundary caught error', error, { 27 errorType: 'react-error', 28 componentStack: errorInfo.componentStack, 29 errorMessage: error.message, 30 errorStack: error.stack, 31 pathname: window.location.pathname, 32 timestamp: Date.now(), 33 }) 34 } 35 36 render() { 37 if (this.state.hasError) { 38 return this.props.fallback || ( 39 <div style={{ padding: '20px', textAlign: 'center' }}> 40 <h2>Something went wrong</h2> 41 <p>We've been notified and are looking into it.</p> 42 <button onClick={() => window.location.reload()}> 43 Reload Page 44 </button> 45 </div> 46 ) 47 } 48 49 return this.props.children 50 } 51} 52 53export default ErrorBoundary
Where to Place Error Boundaries: Wrap strategic parts of your app to isolate failures:
1// App-level boundary 2const App = () => ( 3 <ErrorBoundary> 4 <Router> 5 <Routes /> 6 </Router> 7 </ErrorBoundary> 8) 9 10// Feature-level boundaries 11const Dashboard = () => ( 12 <div> 13 <ErrorBoundary fallback={<WidgetError />}> 14 <RevenueWidget /> 15 </ErrorBoundary> 16 17 <ErrorBoundary fallback={<WidgetError />}> 18 <UsersWidget /> 19 </ErrorBoundary> 20 21 <ErrorBoundary fallback={<WidgetError />}> 22 <ActivityWidget /> 23 </ErrorBoundary> 24 </div> 25)
This approach ensures one broken widget doesn't take down the entire dashboard.
Note on Browser Plugin Integration: While the browser plugin automatically catches unhandled JavaScript errors and promise rejections, Error Boundaries are still essential. The browser plugin catches errors that escape React's component tree, while Error Boundaries catch errors within React components and allow you to show fallback UI. Together, they provide comprehensive error coverage:
- Error Boundaries: Catch React component errors + show fallback UI + log with
logzai.exception() - Browser Plugin: Catch all other JavaScript errors + unhandled promise rejections automatically
Both layers working together ensure nothing slips through the cracks.
2. Log Exceptions with Full Context
When logging exceptions, include everything needed to reproduce and debug the issue:
1const fetchUserProfile = async (userId: string) => { 2 try { 3 const response = await api.get(`/users/${userId}`) 4 return response.data 5 } catch (error) { 6 logzai.exception('Failed to fetch user profile', error, { 7 // Error details 8 errorMessage: error.message, 9 errorCode: error.response?.status, 10 11 // Request context 12 userId, 13 endpoint: `/users/${userId}`, 14 15 // User context (automatically added by contextInjector) 16 // - currentUserId, email 17 // - orgId, orgName 18 // - pathname 19 20 // Additional debugging info 21 retryCount: 0, 22 timestamp: Date.now(), 23 }) 24 25 throw error // Re-throw to let calling code handle 26 } 27}
The logzai.exception() method is specifically designed for errors and automatically extracts the stack trace and error details.
3. Handle Async Errors (Promise Rejections)
Good news: The browser plugin automatically captures unhandled promise rejections! Once you've enabled the browser plugin with logzai.plugin('browser', browserPlugin), all unhandled rejections are automatically logged with full context.
However, you should still handle promise rejections explicitly where possible for better control over error messages and recovery strategies:
1const loadUserData = async () => { 2 try { 3 const [profile, settings, preferences] = await Promise.all([ 4 fetchProfile(), 5 fetchSettings(), 6 fetchPreferences(), 7 ]) 8 9 return { profile, settings, preferences } 10 } catch (error) { 11 logzai.exception('Failed to load user data', error, { 12 failedOperation: 'loadUserData', 13 retryable: true, 14 }) 15 16 // Show user-friendly error 17 throw new Error('Unable to load your profile. Please try again.') 18 } 19}
4. API Error Handling Pattern
Centralize API error logging using interceptors. For axios:
1import axios from 'axios' 2import logzai from 'logzai-js/browser' 3 4const apiClient = axios.create({ 5 baseURL: import.meta.env.VITE_API_URL, 6}) 7 8// Request interceptor (log outgoing requests in debug mode) 9apiClient.interceptors.request.use( 10 (config) => { 11 if (import.meta.env.DEV) { 12 logzai.debug('API request', { 13 method: config.method?.toUpperCase(), 14 url: config.url, 15 params: config.params, 16 }) 17 } 18 return config 19 }, 20 (error) => { 21 logzai.error('API request setup failed', { 22 error: error.message, 23 }) 24 return Promise.reject(error) 25 } 26) 27 28// Response interceptor (log errors) 29apiClient.interceptors.response.use( 30 (response) => response, 31 (error) => { 32 const request = error.config 33 34 logzai.error('API request failed', { 35 method: request?.method?.toUpperCase(), 36 url: request?.url, 37 statusCode: error.response?.status, 38 statusText: error.response?.statusText, 39 errorMessage: error.message, 40 responseData: error.response?.data, 41 requestDuration: Date.now() - request?.metadata?.startTime, 42 }) 43 44 return Promise.reject(error) 45 } 46) 47 48export default apiClient
For fetch API:
1const fetchWithLogging = async (url: string, options?: RequestInit) => { 2 const startTime = Date.now() 3 4 try { 5 const response = await fetch(url, options) 6 7 if (!response.ok) { 8 const errorData = await response.text() 9 10 logzai.error('Fetch request failed', { 11 url, 12 method: options?.method || 'GET', 13 statusCode: response.status, 14 statusText: response.statusText, 15 errorData, 16 duration: Date.now() - startTime, 17 }) 18 19 throw new Error(`HTTP ${response.status}: ${response.statusText}`) 20 } 21 22 return response 23 } catch (error) { 24 logzai.exception('Fetch request error', error, { 25 url, 26 method: options?.method || 'GET', 27 duration: Date.now() - startTime, 28 }) 29 30 throw error 31 } 32}
5. User-Friendly Error Messages
Always separate what you log from what you show users:
1class ErrorBoundary extends Component<Props, State> { 2 // ... previous code 3 4 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 5 // Log technical details 6 logzai.exception('React error boundary caught error', error, { 7 errorType: 'react-error', 8 componentStack: errorInfo.componentStack, 9 errorMessage: error.message, 10 errorStack: error.stack, 11 }) 12 } 13 14 render() { 15 if (this.state.hasError) { 16 // Show friendly message to users 17 return ( 18 <div className="error-container"> 19 <h2>Oops! Something went wrong</h2> 20 <p> 21 We've been notified and are working on a fix. 22 In the meantime, try refreshing the page. 23 </p> 24 <button onClick={() => window.location.reload()}> 25 Refresh Page 26 </button> 27 </div> 28 ) 29 } 30 31 return this.props.children 32 } 33}
Never expose stack traces, error codes, or technical jargon to end users—those belong in your logs, not your UI.
Advanced Patterns
1. Redux/State Management Integration
If you're using Redux, you can log state changes and actions with middleware:
1import { Middleware } from '@reduxjs/toolkit' 2import logzai from 'logzai-js/browser' 3 4const logzaiMiddleware: Middleware = () => (next) => (action) => { 5 // Only log in development or for specific action types 6 if ( 7 import.meta.env.DEV || 8 ['auth/login', 'auth/logout', 'user/update'].includes(action.type) 9 ) { 10 logzai.debug('Redux action dispatched', { 11 kind: 'redux', 12 type: action.type, 13 payload: action.payload, 14 timestamp: Date.now(), 15 }) 16 } 17 18 return next(action) 19} 20 21// Add to your store configuration 22export const store = configureStore({ 23 reducer: rootReducer, 24 middleware: (getDefaultMiddleware) => 25 getDefaultMiddleware().concat(logzaiMiddleware), 26})
When to Use: Enable this in production only for critical actions to avoid log spam. Use it freely in development for debugging.
2. Route Change Logging
Track user navigation to understand user journeys:
1import { useEffect } from 'react' 2import { useLocation } from 'react-router-dom' 3import logzai from 'logzai-js/browser' 4 5const RouteLogger = () => { 6 const location = useLocation() 7 8 useEffect(() => { 9 logzai.info('Route changed', { 10 pathname: location.pathname, 11 search: location.search, 12 hash: location.hash, 13 timestamp: Date.now(), 14 }) 15 }, [location]) 16 17 return null 18} 19 20// Use in your app 21const App = () => ( 22 <Router> 23 <RouteLogger /> 24 <Routes> 25 {/* your routes */} 26 </Routes> 27 </Router> 28)
3. Custom Hooks for Logging
Create reusable hooks to standardize logging patterns:
1import { useCallback } from 'react' 2import { useLocation } from 'react-router-dom' 3import logzai from 'logzai-js/browser' 4 5// Hook for logging user actions 6export const useLogAction = () => { 7 const location = useLocation() 8 9 return useCallback((action: string, context?: Record<string, any>) => { 10 logzai.info(action, { 11 ...context, 12 pathname: location.pathname, 13 timestamp: Date.now(), 14 }) 15 }, [location]) 16} 17 18// Usage in components 19const ShoppingCart = () => { 20 const logAction = useLogAction() 21 22 const handleCheckout = () => { 23 logAction('Checkout initiated', { 24 itemCount: cartItems.length, 25 totalValue: calculateTotal(cartItems), 26 }) 27 28 // ... checkout logic 29 } 30 31 return ( 32 // ... component JSX 33 ) 34}
4. Error Recovery Strategies
Build error boundaries that can recover from errors:
1interface Props { 2 children: ReactNode 3 maxRetries?: number 4} 5 6interface State { 7 hasError: boolean 8 error?: Error 9 retryCount: number 10} 11 12class RecoverableErrorBoundary extends Component<Props, State> { 13 constructor(props: Props) { 14 super(props) 15 this.state = { hasError: false, retryCount: 0 } 16 } 17 18 static getDerivedStateFromError(error: Error): Partial<State> { 19 return { hasError: true, error } 20 } 21 22 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 23 logzai.exception('Recoverable error boundary caught error', error, { 24 errorType: 'react-error', 25 componentStack: errorInfo.componentStack, 26 retryCount: this.state.retryCount, 27 maxRetries: this.props.maxRetries || 3, 28 }) 29 } 30 31 handleRetry = () => { 32 const { maxRetries = 3 } = this.props 33 const newRetryCount = this.state.retryCount + 1 34 35 if (newRetryCount <= maxRetries) { 36 logzai.info('Error boundary retry attempt', { 37 retryCount: newRetryCount, 38 maxRetries, 39 }) 40 41 this.setState({ 42 hasError: false, 43 error: undefined, 44 retryCount: newRetryCount 45 }) 46 } else { 47 logzai.warn('Error boundary max retries reached', { 48 retryCount: newRetryCount, 49 maxRetries, 50 }) 51 } 52 } 53 54 render() { 55 const { hasError, retryCount } = this.state 56 const { maxRetries = 3 } = this.props 57 58 if (hasError) { 59 return ( 60 <div className="error-container"> 61 <h2>Something went wrong</h2> 62 {retryCount < maxRetries ? ( 63 <> 64 <p>Would you like to try again?</p> 65 <button onClick={this.handleRetry}> 66 Try Again ({maxRetries - retryCount} attempts remaining) 67 </button> 68 </> 69 ) : ( 70 <> 71 <p>We've exhausted retry attempts. Please refresh the page.</p> 72 <button onClick={() => window.location.reload()}> 73 Refresh Page 74 </button> 75 </> 76 )} 77 </div> 78 ) 79 } 80 81 return this.props.children 82 } 83}
This pattern is useful for components that might fail due to temporary issues (network glitches, race conditions, etc.).
Production Checklist
Before deploying your logging and error handling to production, verify these items:
Configuration
- [ ] Use environment-specific ingest tokens (different for dev/staging/prod)
- [ ] Set
environmentfield correctly based onNODE_ENV - [ ] Configure appropriate log levels (debug in dev, info/warn/error in prod)
- [ ] Set
mirrorToConsoletofalsein production - [ ] Enable browser plugin with
logzai.plugin('browser', browserPlugin) - [ ] Configure
contextInjectorto include user, org, and route context - [ ] Set up
errorFilterto exclude benign errors (ResizeObserver, etc.)
Testing
- [ ] Test error boundaries with intentionally thrown errors
- [ ] Verify browser plugin captures unhandled JavaScript errors automatically
- [ ] Test that unhandled promise rejections are logged by the browser plugin
- [ ] Verify logs appear in logzai dashboard with correct context
- [ ] Verify
contextInjectoris working (check logs include userId, orgId, route) - [ ] Test
errorFilterexcludes errors you want to skip - [ ] Check that sensitive data is NOT being logged
Monitoring
- [ ] Set up alerts for high error rates
- [ ] Create dashboards for key metrics (error rates by route, user actions)
- [ ] Configure notification channels (email, Slack, PagerDuty)
- [ ] Review logs regularly to identify patterns
Privacy & Security
- [ ] Never log passwords, tokens, or API keys
- [ ] Sanitize PII (email, phone, address) before logging
- [ ] Review data retention policies
- [ ] Ensure compliance with GDPR/CCPA if applicable
Performance
- [ ] Verify bundle size impact (logzai-js is ~15KB gzipped)
- [ ] Use log sampling for high-frequency events
- [ ] Avoid logging large objects or arrays
- [ ] Profile your app to ensure logging doesn't impact performance
Common Pitfalls to Avoid
Over-Logging
Logging every single action creates noise that obscures real issues:
1// ❌ Bad: Too much noise 2const MyComponent = () => { 3 logzai.debug('MyComponent rendering') 4 logzai.debug('Props received', props) 5 logzai.debug('State initialized') 6 7 const handleClick = () => { 8 logzai.debug('Button clicked') 9 logzai.debug('State before update', state) 10 setState(newState) 11 logzai.debug('State after update', newState) 12 } 13 14 logzai.debug('MyComponent render complete') 15 return <button onClick={handleClick}>Click me</button> 16}
Instead, log meaningful events:
1// ✅ Good: Signal over noise 2const MyComponent = () => { 3 const handleClick = () => { 4 logzai.info('Important action triggered', { 5 actionType: 'submit-form', 6 formData: sanitizedData, 7 }) 8 setState(newState) 9 } 10 11 return <button onClick={handleClick}>Click me</button> 12}
Under-Logging
The opposite problem: not logging enough context to be useful:
1// ❌ Bad: Not enough context 2logzai.error('API call failed')
1// ✅ Good: Actionable context 2logzai.error('API call failed', { 3 endpoint: '/api/users', 4 method: 'POST', 5 statusCode: 500, 6 errorMessage: error.message, 7 requestId: response.headers['x-request-id'], 8})
Synchronous Logging in Hot Paths
Don't perform expensive operations in frequently-called code:
1// ❌ Bad: Expensive operation in render 2const ProductList = ({ products }) => { 3 logzai.debug('Rendering products', { 4 products: products.map(p => ({ // Expensive! 5 id: p.id, 6 name: p.name, 7 fullDetails: JSON.stringify(p), // Very expensive! 8 })) 9 }) 10 11 return <div>{/* render products */}</div> 12}
1// ✅ Good: Log only what's necessary 2const ProductList = ({ products }) => { 3 logzai.debug('Rendering products', { 4 productCount: products.length, 5 firstProductId: products[0]?.id, 6 }) 7 8 return <div>{/* render products */}</div> 9}
Logging Sensitive Data
Never log passwords, tokens, credit cards, or other sensitive information:
1// ❌ Bad: Logging sensitive data 2logzai.info('User logged in', { 3 email: user.email, 4 password: user.password, // NEVER DO THIS! 5 creditCard: user.paymentMethod.cardNumber, // NEVER DO THIS! 6})
1// ✅ Good: Sanitized logging 2logzai.info('User logged in', { 3 userId: user.id, 4 email: user.email, 5 hasPaymentMethod: !!user.paymentMethod, 6 loginMethod: 'password', 7})
Ignoring Error Boundaries
Don't let errors crash your entire app:
1// ❌ Bad: No error boundary 2const App = () => ( 3 <Router> 4 <Routes> 5 <Route path="/" element={<Dashboard />} /> 6 {/* One error here crashes everything */} 7 </Routes> 8 </Router> 9)
1// ✅ Good: Strategic error boundaries 2const App = () => ( 3 <ErrorBoundary> 4 <Router> 5 <Routes> 6 <Route path="/" element={ 7 <ErrorBoundary fallback={<DashboardError />}> 8 <Dashboard /> 9 </ErrorBoundary> 10 } /> 11 </Routes> 12 </Router> 13 </ErrorBoundary> 14)
Generic Error Messages
Don't make debugging harder with vague errors:
1// ❌ Bad: Generic error 2throw new Error('Something went wrong')
1// ✅ Good: Specific error 2throw new Error( 3 `Failed to load user profile: ${error.message} (User ID: ${userId})` 4)
Conclusion
Implementing robust logging and error handling in React applications isn't optional—it's essential for building reliable, maintainable software. Let's recap the key takeaways:
- Enable the browser plugin for automatic error capture—it catches JavaScript errors and promise rejections with zero extra code
- Use structured logging with rich context via the
contextInjectorto make logs searchable and actionable - Implement error boundaries at strategic points to prevent cascading failures and show fallback UI
- Log at appropriate levels to maintain signal-to-noise ratio
- Handle specific errors explicitly for better control, while the browser plugin catches everything else
- Separate technical logs from user-facing messages to maintain good UX
- Test your error handling to ensure it works when things go wrong
- Monitor and alert on error patterns to catch issues proactively
The logzai browser plugin is a game-changer: it automatically captures errors you didn't anticipate, enriches them with application context, and sends them to your dashboard—all without blocking your UI or requiring manual instrumentation throughout your codebase.
By following these patterns with logzai, you'll transform debugging from a guessing game into a data-driven process. You'll catch bugs faster, understand user behavior better, and ship more reliable software.
Ready to level up your React app's observability? Try logzai free and start logging like a pro. For more advanced patterns and integration examples, check out our complete documentation.
Happy logging!