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 getDerivedStateFromErrorandcomponentDidCatch.
- 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 catchhandler.
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 infolevel.
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.