Move beyond try/catch and embrace safer, more maintainable error handling using the Result type pattern.
TypeScript’s traditional try/catch mechanisms often leave error handling optional and runtime prone to unhandled exceptions. The Result type pattern enforces explicit handling, reducing surprises and improving code reliability.
đź§ What Is a Result Type?
A Result type is a discriminated union that clearly expresses success or failure via structured return values:
type Result<T, E = Error>
= { ok: true; data: T }
| { ok: false; error: E };
Instead of throwing, functions return an object with either a data
payload or an error
, forcing callers to handle both paths explicitly.
âś… Example: Fetching a User Safely
function getUser(id: number): Result<User> {
if (id === 0) {
return { ok: false, error: new Error('User not found') };
}
return { ok: true, data: { id, name: 'Sample User' } };
}
const result = getUser(0);
if (result.ok) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
This removes implicit exceptions—every outcome is handled.
🔄 Benefits of the Result Pattern
- Type-safety: TypeScript ensures exhaustive checks on success and failure cases.
- No hidden exceptions: Errors are values, not thrown surprises.
- Clear intention: Function signatures show exactly what can go wrong.
- Consistency: All functions return in a predictable format—no guesswork needed.
⚙️ Advanced Handling with Libraries
For larger applications, consider using libraries like neverthrow
, which provide:
- Functional patterns like
map
,mapErr
, andmatch
- Async-friendly APIs like
AsyncResult
- Better integration with functional programming tools
- Integration with ESLint for enforcing return checking
đź”§ Type Safety & Error Classification
Define custom error types with discriminants to enable smart type narrowing and more descriptive failure states:
class NotFoundError extends Error {
readonly type = 'not-found';
}
class PermissionDeniedError extends Error {
readonly type = 'permission-denied';
}
type CustomError = NotFoundError | PermissionDeniedError;
function lookup(): Result<Data, CustomError> {
return { ok: false, error: new NotFoundError('Missing item') };
}
const res = lookup();
if (res.ok) {
// handle success
} else if (res.error.type === 'not-found') {
console.warn('Item not found:', res.error.message);
} else if (res.error.type === 'permission-denied') {
alert('Access denied');
}
đź§© Integrating with Async Code
Pair Result with async logic for safer data fetching and better control of network behavior:
async function fetchData(): Promise<Result<Data>> {
try {
const resp = await fetch('/data');
const json = await resp.json();
return { ok: true, data: json };
} catch (e) {
return {
ok: false,
error: e instanceof Error ? e : new Error(String(e))
};
}
}
đź§Ş Testing Result-Based Functions
Testing functions that return Result types is simpler than testing ones that throw:
test('returns data on success', () => {
const result = getUser(1);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data.name).toBe('Sample User');
}
});
test('returns error on failure', () => {
const result = getUser(0);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toBe('User not found');
}
});
🎨 Integrating with React UI
You can directly use Result types in components for conditional rendering:
const { ok, data, error } = await fetchData();
return (
<>
{ok ? (
<div>Welcome, {data.name}</div>
) : (
<div className="error">Error: {error.message}</div>
)}
</>
);
🛡 Best Practices
- Prefer
Result<T, E>
over throwing exceptions for expected errors. - Define custom error types for clarity and maintainability.
- Use utility libraries like
neverthrow
oroxide.ts
to avoid boilerplate. - Centralize your Result handling logic in shared service layers.
- Use exhaustive checks in your UI and API layers to prevent edge cases.
📌 Takeaways
- Use Result types to make error cases explicit and type-safe.
- Avoid hidden exceptions with structured return values.
- Adopt libraries like
neverthrow
for chaining, async support, and enforcement. - Improve testing, UI handling, and long-term maintainability with predictable return types.
At Web Expert Solution, we champion clean, maintainable TypeScript patterns. Subscribe for more on robust architectures, progressive design patterns, and practical developer workflows!