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 sweetalert2Only needed for the 502 modal. If you prefer a Mantine Modal, replace the Swal.fire(...) call — the pattern is the same.
Guidelines#
- Return
trueto consume the error. When the handler returnstrue, the store swallows the exception — the calling component never sees it. Returnfalseto let the component handle it (useful for custom per-call error UX). - Use
toastIdfor deduplication.react-toastifydeduplicates bytoastId— 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_MSwindow. - Don’t swallow everything. Unknown errors (
default: return false) should propagate so components can render inline error states when needed.
See also: PalmyraStoreFactory.