Skip to content

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'
);
}
};

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.


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);
}
});

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);
}
};
// Usage
const App = (svc: Service) => () => {
const { el } = svc;
const withErrorBoundary = svc(errorBoundary);
const useRiskyComponent = svc(RiskyComponent);
return el('div')(
withErrorBoundary(
() => useRiskyComponent(),
(e) => el('div')(`Error: ${e}`)
)
);
};

Catching errors without logging or re-throwing hides bugs:

// ❌ WRONG - error silently disappears
const 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 it
const 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 computations
const 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 throwing
type 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 };
});
// Usage
match(derived, (result) =>
result.ok
? el('span')(result.value)
: el('span').props({ className: 'error' })(result.error)
);

Resources track errors automatically. Ignoring them causes crashes:

// ❌ WRONG - assumes data is always available
const 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 explicitly
const 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 limbo
const 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 separately
const 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')
);
};