Error Handling
In Rimitive, errors are plain JavaScript errors. Handle them with try/catch:
const SafeWrapper = (svc: Service) => () => { const { el } = svc; const useRiskyComponent = svc(RiskyComponent);
try { return useRiskyComponent(); } catch (e) { return el('div').props({ className: 'error' })( 'Something went wrong' ); }};Handling Async Errors
Section titled “Handling Async Errors”For async operations, use the resource module. It tracks error state explicitly:
const ProductList = (svc: Service) => () => { const { el, resource, match } = svc;
const products = resource((signal) => fetch('/api/products', { signal }).then(r => r.json()) );
return match(products, (state) => { if (state.status === 'pending') { return el('div')('Loading...'); } if (state.status === 'error') { return el('div').props({ className: 'error' })( `Failed to load: ${state.error}` ); } return el('ul')( ...state.value.map(p => el('li')(p.name)) ); });};The error state is part of the resource’s reactive value.
Effect Errors
Section titled “Effect Errors”Errors in effects propagate normally. If you need to catch them:
import { compose } from '@rimitive/core';import { SignalModule, EffectModule } from '@rimitive/signals/extend';
const svc = compose(SignalModule, EffectModule)();const { signal, effect } = svc;
const count = signal(0);
effect(() => { try { riskyOperation(count()); } catch (e) { console.error('Effect failed:', e); // Handle gracefully }});Or wrap the risky operation in a function that returns a result type:
type Result<T> = { ok: true; value: T } | { ok: false; error: unknown };
const safeRiskyOperation = (n: number): Result<string> => { try { return { ok: true, value: riskyOperation(n) }; } catch (e) { return { ok: false, error: e }; }};
effect(() => { const result = safeRiskyOperation(count()); if (!result.ok) { errorState(result.error); }});Creating a Reusable Error Wrapper
Section titled “Creating a Reusable Error Wrapper”If you want an Error Boundary-like pattern, create a behavior:
const errorBoundary = (svc: Service) => <T>( render: () => T, fallback: (error: unknown) => T ): T => { try { return render(); } catch (e) { return fallback(e); } };
// Usageconst App = (svc: Service) => () => { const { el } = svc; const withErrorBoundary = svc(errorBoundary); const useRiskyComponent = svc(RiskyComponent);
return el('div')( withErrorBoundary( () => useRiskyComponent(), (e) => el('div')(`Error: ${e}`) ) );};Anti-patterns
Section titled “Anti-patterns”Don’t Swallow Errors Silently
Section titled “Don’t Swallow Errors Silently”Catching errors without logging or re-throwing hides bugs:
// ❌ WRONG - error silently disappearsconst LoadData = (svc: Service) => () => { const { el, signal, effect } = svc; const data = signal<Data | null>(null);
effect(() => { try { data(fetchData()); } catch (e) { // Silently swallowed - you'll never know it failed! } });
return el('div')(/* ... */);};// ✅ CORRECT - track error state, show it to users, log itconst LoadData = (svc: Service) => () => { const { el, signal, effect, match } = svc; const data = signal<Data | null>(null); const error = signal<Error | null>(null);
effect(() => { try { error(null); data(fetchData()); } catch (e) { console.error('Failed to load data:', e); error(e instanceof Error ? e : new Error(String(e))); } });
return match(error, (err) => err ? el('div').props({ className: 'error' })(err.message) : el('div')(/* render data */) );};Don’t Throw in Computeds Without Handling
Section titled “Don’t Throw in Computeds Without Handling”A throwing computed breaks any effect or computed that depends on it:
// ❌ WRONG - throws in computed, breaks dependent computationsconst derived = computed(() => { const value = source(); if (value < 0) { throw new Error('Invalid value'); // Breaks everything downstream } return value * 2;});// ✅ CORRECT - return error state instead of throwingtype ComputedResult<T> = { ok: true; value: T } | { ok: false; error: string };
const derived = computed((): ComputedResult<number> => { const value = source(); if (value < 0) { return { ok: false, error: 'Value must be non-negative' }; } return { ok: true, value: value * 2 };});
// Usagematch(derived, (result) => result.ok ? el('span')(result.value) : el('span').props({ className: 'error' })(result.error));Don’t Ignore Resource Error State
Section titled “Don’t Ignore Resource Error State”Resources track errors automatically. Ignoring them causes crashes:
// ❌ WRONG - assumes data is always availableconst ProductList = (svc: Service) => () => { const { el, resource, map } = svc; const products = resource((s) => fetchProducts(s));
// Crashes when in error state! return el('ul')( map(products.data()!, (p) => el('li')(p.name)) );};// ✅ CORRECT - handle error state explicitlyconst ProductList = (svc: Service) => () => { const { el, resource, match, map } = svc; const products = resource((s) => fetchProducts(s));
return match(products, (state) => { switch (state.status) { case 'pending': return el('div')('Loading...'); case 'error': return el('div').props({ className: 'error' })( `Failed to load: ${state.error.message}` ); case 'ready': return el('ul')(map(state.value, (p) => el('li')(p.name))); } });};Don’t Catch and Continue Without Recovery
Section titled “Don’t Catch and Continue Without Recovery”Catching an error but continuing as if nothing happened leads to inconsistent state:
// ❌ WRONG - catches error but leaves state in limboconst SaveForm = (svc: Service) => () => { const { el, signal } = svc; const saving = signal(false);
const save = async () => { saving(true); try { await submitForm(); } catch (e) { // Error caught, but saving is still true! console.error(e); } // Only resets if no error - state is inconsistent saving(false); };
return el('form')(/* ... */);};// ✅ CORRECT - always reset state in finally, track error separatelyconst SaveForm = (svc: Service) => () => { const { el, signal, match } = svc; const saving = signal(false); const saveError = signal<Error | null>(null);
const save = async () => { saving(true); saveError(null); try { await submitForm(); } catch (e) { saveError(e instanceof Error ? e : new Error(String(e))); } finally { saving(false); // Always resets, regardless of success/failure } };
return el('form')( match(saveError, (err) => err ? el('div').props({ className: 'error' })(err.message) : null ), el('button').props({ disabled: saving })('Save') );};