JavaScript and React course Β· Module 16: Error Handling and Suspense

Error Logging and Monitoring

5 min read
In this lesson5

On a spacecraft, every failure is recorded in the black box. Without it, the crew couldn't analyze the causes of problems after the fact. In React applications, we have exactly the same problem -- if an error occurs for a user in production and we don't log it, we'll never know about it. Error logging and monitoring is your production black box.

Error Logging Strategies

In a production environment, we need several layers of logging:

1. console.error -- Only for Development

console.error is useful during local development, but in production the user doesn't see it, and you don't have access to it.

1// Good for development, useless in production
2componentDidCatch(error, errorInfo) {
3  console.error('Error caught:', error);
4  console.error('Component stack:', errorInfo.componentStack);
5}

2. Logging to an External Service

In production, errors must be sent to a monitoring service:

1class ErrorBoundary extends React.Component {
2  componentDidCatch(error, errorInfo) {
3    // Send to monitoring service
4    logErrorToService({
5      message: error.message,
6      stack: error.stack,
7      componentStack: errorInfo.componentStack,
8      timestamp: new Date().toISOString(),
9      url: window.location.href,
10      userAgent: navigator.userAgent,
11    });
12  }
13
14  // ...render with fallback
15}
16
17async function logErrorToService(errorData) {
18  try {
19    await fetch('/api/errors', {
20      method: 'POST',
21      headers: { 'Content-Type': 'application/json' },
22      body: JSON.stringify(errorData),
23    });
24  } catch (e) {
25    // Fallback -- don't block the app if logging fails
26    console.error('Failed to log error:', e);
27  }
28}

Sentry and Error Tracking Services

Sentry is the most popular service for monitoring errors in JavaScript and React applications. Other popular tools include LogRocket, Bugsnag, Datadog, and New Relic.

Basic Sentry Integration

1import * as Sentry from '@sentry/react';
2
3// Initialization in main/index.js
4Sentry.init({
5  dsn: 'https://key@sentry.io/project',
6  environment: 'production',
7  // Percentage of sessions to record (performance)
8  tracesSampleRate: 0.2,
9});
10
11// Sentry automatically captures:
12// - Unhandled exceptions
13// - Unhandled Promise rejections
14// - Network errors

Sentry with Error Boundary

1import * as Sentry from '@sentry/react';
2
3// Sentry provides a ready-made Error Boundary
4function App() {
5  return (
6    <Sentry.ErrorBoundary
7      fallback={({ error }) => (
8        <div className="error-page">
9          <h2>Something went wrong</h2>
10          <p>The error has been automatically reported</p>
11          <button onClick={() => window.location.reload()}>
12            Refresh page
13          </button>
14        </div>
15      )}
16    >
17      <SpaceStation />
18    </Sentry.ErrorBoundary>
19  );
20}

Context and Breadcrumbs in Sentry

Sentry allows you to add context to errors -- information about the user, recent actions (breadcrumbs), and additional data:

1// Set user context
2Sentry.setUser({
3  id: user.id,
4  email: user.email,
5  username: user.callsign,
6});
7
8// Manually add breadcrumbs (user action trail)
9Sentry.addBreadcrumb({
10  category: 'navigation',
11  message: 'User navigated to mission panel',
12  level: 'info',
13});
14
15// Manually report an error with additional context
16Sentry.captureException(error, {
17  extra: {
18    missionId: currentMission.id,
19    moduleState: moduleStatus,
20  },
21});

Custom Error Logging Hook

You can create your own hook that centralizes logging logic:

1function useErrorLogger() {
2  const logError = useCallback((error, context = {}) => {
3    const errorReport = {
4      message: error.message,
5      stack: error.stack,
6      timestamp: new Date().toISOString(),
7      url: window.location.href,
8      ...context,
9    };
10
11    // In production, send to the service
12    if (process.env.NODE_ENV === 'production') {
13      sendToErrorService(errorReport);
14    }
15
16    // In development, log to console
17    console.error('[ErrorLogger]', errorReport);
18  }, []);
19
20  return { logError };
21}
22
23// Usage in a component
24function MissionControl() {
25  const { logError } = useErrorLogger();
26
27  const handleDataFetch = async () => {
28    try {
29      const data = await fetchMissionData();
30      setMissions(data);
31    } catch (error) {
32      logError(error, {
33        component: 'MissionControl',
34        action: 'fetchMissionData',
35      });
36      setError('Failed to fetch mission data');
37    }
38  };
39}

Error Reporting in Production

Global Error Handlers

In addition to Error Boundaries and try-catch, it's worth adding global handlers that catch errors escaping other mechanisms:

1// Global catching of uncaught errors
2window.addEventListener('error', (event) => {
3  logErrorToService({
4    type: 'uncaught_error',
5    message: event.message,
6    filename: event.filename,
7    lineno: event.lineno,
8    colno: event.colno,
9  });
10});
11
12// Global catching of unhandled Promise rejections
13window.addEventListener('unhandledrejection', (event) => {
14  logErrorToService({
15    type: 'unhandled_rejection',
16    message: event.reason?.message || 'Unknown rejection',
17    stack: event.reason?.stack,
18  });
19});

Error Severity Levels

Not all errors are equally important. Error tracking services allow classification:

1// Critical error - application is non-functional
2Sentry.captureException(error, { level: 'fatal' });
3
4// Error - functionality is broken, but app works
5Sentry.captureException(error, { level: 'error' });
6
7// Warning - something might be wrong
8Sentry.captureMessage('API responding slowly', 'warning');
9
10// Info - useful for debugging
11Sentry.captureMessage('User changed language', 'info');

Source Maps for Production Debugging

Production code is minified -- the stack trace is unreadable without source maps. Source maps are like a decoding map -- they allow you to translate positions in minified code back to the original source files.

1// Without source maps, the stack trace looks like:
2// Error at e.render (main.a1b2c3.js:1:45678)
3
4// With source maps:
5// Error at MissionControl.render (MissionControl.jsx:42:15)

Source Maps Configuration

1// webpack.config.js
2module.exports = {
3  devtool: 'source-map', // Generate source maps
4  // ...
5};
6
7// Important: source maps should NOT be public!
8// Upload them to Sentry, but don't serve them to users:
9// sentry-cli sourcemaps upload ./build

Source maps allow you to see exactly which line of original code caused the error, even if the production bundle is minified into a single line. It's like having the technical blueprint of a spacecraft -- instead of looking at an incomprehensible minified panel, you see exactly which module failed and why. Remember, however, to never make source maps publicly available -- that would be like sharing the ship's blueprints with potential invaders.

Code for this lesson: App.jsx
1import React, { useState, useCallback, useEffect } from 'react';
2import './styles.css';
3
4// Custom hook for logging errors
5function useErrorLogger() {
6  const [errorLog, setErrorLog] = useState([]);
7
8  const logError = useCallback((error, context = {}) => {
9    const report = {
10      id: Date.now(),
11      message: error.message || String(error),
12      stack: error.stack ? error.stack.split('\n').slice(0, 3).join('\n') : 'none',
13      timestamp: new Date().toLocaleTimeString(),
14      url: window.location.href,
15      ...context,
16    };
17
18    setErrorLog(prev => [report, ...prev].slice(0, 10));
19
20    // In production we would send this to a service:
21    // sendToSentry(report);
22    console.error('[ErrorLogger]', report);
23  }, []);
24
25  return { logError, errorLog };
26}
27
28// Error Boundary with logging
29class LoggingErrorBoundary extends React.Component {
30  constructor(props) {
31    super(props);
32    this.state = { hasError: false, error: null };
33  }
34
35  static getDerivedStateFromError(error) {
36    return { hasError: true, error };
37  }
38
39  componentDidCatch(error, errorInfo) {
40    // Logging to the "service"
41    if (this.props.onError) {
42      this.props.onError(error, {
43        component: this.props.label || 'Unknown',
44        type: 'render_error',
45        componentStack: errorInfo.componentStack,
46      });
47    }
48  }
49
50  render() {
51    if (this.state.hasError) {
52      return (
53        <div className="error-card">
54          <strong>{this.props.label}: Failure</strong>
55          <p>{this.state.error?.message}</p>
56          <button onClick={() => this.setState({ hasError: false })}>
57            Retry
58          </button>
59        </div>
60      );
61    }
62    return this.props.children;
63  }
64}
65
66// Module that can throw an error
67function UnstableModule({ name, failRate = 0.3 }) {
68  if (Math.random() < failRate) {
69    throw new Error(name + ': critical system failure!');
70  }
71  return (
72    <div className="module-ok">
73      <span className="dot green" /> {name}: Online
74    </div>
75  );
76}
77
78// Component with an async error
79function DataPanel({ logError }) {
80  const [data, setData] = useState(null);
81  const [loading, setLoading] = useState(false);
82
83  const fetchData = async () => {
84    setLoading(true);
85    try {
86      await new Promise(r => setTimeout(r, 800));
87      if (Math.random() > 0.5) {
88        throw new Error('Database connection timeout');
89      }
90      setData({ missions: 42, crew: 7, fuel: '87%' });
91    } catch (error) {
92      logError(error, {
93        component: 'DataPanel',
94        action: 'fetchData',
95        type: 'async_error',
96      });
97      setData(null);
98    } finally {
99      setLoading(false);
100    }
101  };
102
103  return (
104    <div className="data-panel">
105      <h3>Station data</h3>
106      <button onClick={fetchData} className="fetch-btn">
107        {loading ? 'Loading...' : 'Fetch data'}
108      </button>
109      {data && (
110        <ul>
111          <li>Missions: {data.missions}</li>
112          <li>Crew: {data.crew}</li>
113          <li>Fuel: {data.fuel}</li>
114        </ul>
115      )}
116    </div>
117  );
118}
119
120export default function App() {
121  const { logError, errorLog } = useErrorLogger();
122  const [key, setKey] = useState(0);
123
124  // Global handler (simulation)
125  useEffect(() => {
126    const handler = (event) => {
127      logError(new Error(event.reason?.message || 'Unhandled rejection'), {
128        type: 'unhandled_rejection',
129      });
130    };
131    window.addEventListener('unhandledrejection', handler);
132    return () => window.removeEventListener('unhandledrejection', handler);
133  }, [logError]);
134
135  return (
136    <div className="app">
137      <h1>Error Logging & Monitoring</h1>
138      <p className="subtitle">
139        Every error is logged (black box)
140      </p>
141      <button onClick={() => setKey(k => k + 1)} className="reset-btn">
142        Reset modules
143      </button>
144
145      <div className="modules" key={key}>
146        <LoggingErrorBoundary label="Navigation" onError={logError}>
147          <UnstableModule name="Navigation" />
148        </LoggingErrorBoundary>
149
150        <LoggingErrorBoundary label="Communication" onError={logError}>
151          <UnstableModule name="Communication" />
152        </LoggingErrorBoundary>
153
154        <DataPanel logError={logError} />
155      </div>
156
157      <div className="log-panel">
158        <h3>Error log ({errorLog.length})</h3>
159        {errorLog.length === 0 ? (
160          <p className="empty-log">No errors recorded</p>
161        ) : (
162          errorLog.map(entry => (
163            <div key={entry.id} className="log-entry">
164              <span className="log-time">{entry.timestamp}</span>
165              <span className="log-type">[{entry.type || 'error'}]</span>
166              <span className="log-msg">{entry.message}</span>
167              {entry.component && (
168                <span className="log-comp">({entry.component})</span>
169              )}
170            </div>
171          ))
172        )}
173      </div>
174    </div>
175  );
176}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Why is console.error insufficient for monitoring errors in production?

  2. 2. How does Sentry integrate with Error Boundaries in React?

Hands-on tasks in the game

  • Vertical ordering

    Order error severity levels from highest to lowest:

  • Code editor

    Implement the useErrorLogger hook

  • Click in order

    Click elements in order for building error logging in componentDidCatch:

  • Horizontal ordering

    Arrange the correct syntax for a global Promise rejection handler:

Useful articles