JavaScript and React course Β· Module 16: Error Handling and Suspense
Error Logging and Monitoring
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 errorsSentry 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 ./buildSource 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. Why is console.error insufficient for monitoring errors in production?
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: