React - Error Reporting Patterns
Overview
Estimated time: 25–35 minutes
Combine an Error Boundary with global error handlers to capture and report errors consistently.
Try it: Error boundary + window error hooks
View source
function createLogger(send){
let seq = 0; return { info(e,d){ send({lvl:'info', ts:Date.now(), seq:++seq, event:e, ...d}); }, error(e,d){ send({lvl:'error', ts:Date.now(), seq:++seq, event:e, ...d}); } };
}
class ErrorBoundary extends React.Component{
constructor(p){ super(p); this.state = { hasError:false, err:null}; }
static getDerivedStateFromError(err){ return { hasError:true, err }; }
componentDidCatch(error, info){ this.props.onError?.(error, info); }
render(){ if (this.state.hasError){ return this.props.fallback ?? React.createElement('div', null, 'Something went wrong'); } return this.props.children; }
}
function Buggy(){ throw new Error('Render error'); }
function App(){
const [logs, setLogs] = React.useState([]);
const logger = React.useMemo(() => createLogger(entry => setLogs(ls => [...ls, entry])), []);
React.useEffect(() => {
function onErr(msg, src, line, col, err){ logger.error('window:error',{msg:String(msg), src, line, col, name:err?.name, message:err?.message}); }
function onRej(e){ logger.error('window:unhandledrejection',{reason: String(e.reason)}); }
window.addEventListener('error', onErr);
window.addEventListener('unhandledrejection', onRej);
return () => { window.removeEventListener('error', onErr); window.removeEventListener('unhandledrejection', onRej); };
}, [logger]);
function throwInHandler(){ throw new Error('Event handler crash'); }
function unhandledPromise(){ Promise.reject('Oops (promise)'); }
return (
<div>
<div style={{display:'flex', gap:8, flexWrap:'wrap'}}>
<button onClick={throwInHandler}>Throw in handler</button>
<button onClick={unhandledPromise}>Unhandled rejection</button>
</div>
<div style={{marginTop:8, padding:8, border:'1px dashed var(--border)'}}>
<ErrorBoundary fallback={<div>Fallback UI</div>} onError={(err, info) => logger.error('boundary:error',{name:err.name, message:err.message, stack:err.stack?.split('\n')[0]})}>
<Buggy />
</ErrorBoundary>
</div>
<h4>Logs</h4>
<ol>{logs.map((l,i)=> <li key={i}>{l.lvl.toUpperCase()} {l.event} {JSON.stringify(l)}</li>)}</ol>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-errors')).render(<App />);
Syntax primer
- Error Boundary: class component with
getDerivedStateFromError
andcomponentDidCatch
. - Global handlers:
window.onerror
/window.addEventListener('error')
andunhandledrejection
. - Structured logging: unify fields for error events.
Vocabulary
- Error Boundary: catches errors in child tree during render, lifecycle, and constructors.
- Unhandled rejection: a Promise rejection without a
catch
handler.
Common pitfalls
- Error Boundaries do not catch errors inside event handlers; handle those separately.
- Don’t leak sensitive data in error logs; scrub payloads.
Exercises
- Send error logs to a mock endpoint (append to a second list) and batch them.
- Include the current route hash and user ID with each error.
- Wrap async component code with try/catch and log expected failures at
info
level.
Checks for Understanding
- What kinds of errors do Error Boundaries catch, and what do they not catch?
- How can you capture global errors and unhandled promise rejections?
- What practices help ensure sensitive data isn’t leaked in error logs?
Answers
- They catch errors during render, lifecycle, and constructors of the child tree. They don’t catch errors in event handlers or async code outside render.
- Attach listeners for 'error' and 'unhandledrejection' on window and normalize/log them in a single place.
- Scrub payloads, avoid sending PII, and limit stack traces/contexts to what’s necessary for debugging; redact tokens/IDs.