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 and componentDidCatch.
  • Global handlers: window.onerror/window.addEventListener('error') and unhandledrejection.
  • 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

  1. Send error logs to a mock endpoint (append to a second list) and batch them.
  2. Include the current route hash and user ID with each error.
  3. Wrap async component code with try/catch and log expected failures at info level.

Checks for Understanding

  1. What kinds of errors do Error Boundaries catch, and what do they not catch?
  2. How can you capture global errors and unhandled promise rejections?
  3. What practices help ensure sensitive data isn’t leaked in error logs?
Answers
  1. 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.
  2. Attach listeners for 'error' and 'unhandledrejection' on window and normalize/log them in a single place.
  3. Scrub payloads, avoid sending PII, and limit stack traces/contexts to what’s necessary for debugging; redact tokens/IDs.