import axios from 'axios' import { isChunkLoadError, reloadOnceForChunk } from './chunkReload' /** * The half no boundary can reach: throws in event handlers, in async code, and * rejected promises nobody caught. * * Deliberately quiet. It logs, and it heals one specific case — a dynamic import * that failed because the chunk is gone after a deploy. It does not toast and it * does not redirect: the axios interceptor already redirects on 401/403 and * rewrites 429 (api/client.ts), and there are 600+ `toast.error` call sites that * report their own failures. Reacting again here would mean double toasts and, in * the redirect cases, redirect loops. */ /** Axios rejects onward from its interceptor, so every caller without a catch lands here. */ function isHandledNetworkError(reason: unknown): boolean { return axios.isAxiosError(reason) } function onUnhandledRejection(event: PromiseRejectionEvent): void { const reason = event.reason if (isChunkLoadError(reason)) { // A lazy import can reject outside render too — a prefetch, or a chunk pulled // in from an effect. Same cure, same once-per-session guard. reloadOnceForChunk() return } if (isHandledNetworkError(reason)) return console.error('[unhandledrejection]', reason) } function onError(event: ErrorEvent): void { if (isChunkLoadError(event.error ?? event.message)) { reloadOnceForChunk() return } console.error('[window.onerror]', event.error ?? event.message) } /** * Call once, before the app renders. Returns a teardown so tests can install and * remove it without leaking listeners between files. */ export function installGlobalErrorHandlers(): () => void { window.addEventListener('unhandledrejection', onUnhandledRejection) // Bubble phase only: the capture phase would also fire for every failed // and , which on a map-heavy app means hundreds of tile requests. window.addEventListener('error', onError) return () => { window.removeEventListener('unhandledrejection', onUnhandledRejection) window.removeEventListener('error', onError) } }