Production error handling with PalmyraStoreFactory#

The default error handler in a fresh PalmyraStoreFactory does nothing — errors propagate to the component that triggered the call. In production you want a centralized handler that differentiates status codes, throttles toast spam, and routes auth failures to login.

The pattern#

// src/wire/StoreFactory.ts
import { PalmyraStoreFactory } from '@palmyralabs/palmyra-wire';
import { toast } from 'react-toastify';
import Swal from 'sweetalert2';

let lastServerErrorAt = 0;
const COOLDOWN_MS = 5000;

const errorHandler = () => (error: any) => {
  const status = error?.response?.status;

  switch (status) {
    case 401:
      // Session expired — redirect to login
      window.dispatchEvent(new CustomEvent('session:expired'));
      window.location.assign('/login');
      return true;

    case 403:
      // ACL denied
      toast.error("You don't have permission for this action.", { toastId: 'acl-403' });
      return true;

    case 400:
      // Validation error — show the server's message if available
      const msg = error?.response?.data?.message;
      toast.error(msg || 'Invalid request. Please check your input.', { toastId: 'val-400' });
      return true;

    case 500:
    case 503: {
      // Server error with cooldown — prevent toast spam when every grid/form fails at once
      const now = Date.now();
      if (now - lastServerErrorAt > COOLDOWN_MS) {
        toast.error('Server error. Please retry in a moment.');
        lastServerErrorAt = now;
      }
      return true;
    }

    case 502: {
      // Server down — show a modal instead of a toast (more prominent)
      Swal.fire({
        icon: 'warning',
        title: 'Server Unreachable',
        text: 'The server is currently unavailable. Please try again later.',
        confirmButtonText: 'OK',
      });
      return true;
    }

    default:
      // Unknown error — let it propagate to the component
      return false;
  }
};

const AppStoreFactory = new PalmyraStoreFactory({
  baseUrl: '/api',
  errorHandlerFactory: errorHandler,
});

export default AppStoreFactory;

What each status does#

Status UX Why this approach
401 Redirect to /login Session is gone — no point showing a toast the user can’t act on
403 Toast with toastId (deduped) Tell the user they lack permission; toastId prevents duplicate toasts if multiple calls fail simultaneously
400 Toast with server message Server validation errors carry a meaningful message — show it
500 / 503 Toast with 5-second cooldown When the server is struggling, every pending request fails — without cooldown you’d see 10 identical toasts
502 SweetAlert2 modal Server is completely down — a modal is more visible than a toast and blocks further action

Listening for session expiry elsewhere#

// In your App or layout component
useEffect(() => {
  const handler = () => {
    // Clear local auth state, ACL cache, etc.
    localStorage.clear();
  };
  window.addEventListener('session:expired', handler);
  return () => window.removeEventListener('session:expired', handler);
}, []);

Install SweetAlert2#

npm install sweetalert2

Only needed for the 502 modal. If you prefer a Mantine Modal, replace the Swal.fire(...) call — the pattern is the same.

Guidelines#

  • Return true to consume the error. When the handler returns true, the store swallows the exception — the calling component never sees it. Return false to let the component handle it (useful for custom per-call error UX).
  • Use toastId for deduplication. react-toastify deduplicates by toastId — prevents 10 identical “403” toasts when a grid + 3 lookups all fail at once.
  • Cooldown for 5xx. A timestamp check is simpler and more reliable than a debounce — it guarantees at most one toast per COOLDOWN_MS window.
  • Don’t swallow everything. Unknown errors (default: return false) should propagate so components can render inline error states when needed.

See also: PalmyraStoreFactory.