
The Problem
In real-world apps, errors can come from:
- User logic issues (e.g., invalid input, missing config)
- System or unknown issues (e.g., network failures, unexpected nulls)
We want to:
- Show friendly error messages for known issues.
- Log and monitor unexpected ones for debugging.
- Keep our code clean and predictable.
✅ Step 1: Define Your Error Classes
Use custom classes to differentiate known and unknown errors.
typescript
class DefinedError extends Error {
code: string;
constructor(message: string, code: string = 'DEFINED_ERROR') {
super(message);
this.name = 'DefinedError';
this.code = code;
}
}
class UnexpectedError extends Error {
constructor(message: string = 'An unexpected error occurred') {
super(message);
this.name = 'UnexpectedError';
}
}
✅ Step 2: Throw the Right Error Type
Here’s how you can throw and wrap errors based on what goes wrong:
typescript
async function riskyOperation(): Promise<string> {
try {
// Simulate known failure
const shouldFail = Math.random() > 0.5;
if (shouldFail) {
throw new DefinedError('Invalid operation: Something went wrong.', 'INVALID_ACTION');
}
// Simulate success
return 'Success!';
} catch (error) {
if (error instanceof DefinedError) {
throw error; // Forward known error
}
// Wrap unexpected issues
throw new UnexpectedError(error instanceof Error ? error.message : String(error));
}
}
✅ Step 3: Handle with Grace
Now you can catch and respond differently based on the type:
typescript
try {
const result = await riskyOperation();
console.log('✅ Result:', result);
} catch (error) {
if (error instanceof DefinedError) {
console.warn(`[Handled] ${error.code}: ${error.message}`);
// Show user-friendly message
} else if (error instanceof UnexpectedError) {
console.error(`[Unexpected] ${error.message}`);
// Report to Sentry or monitoring tools
} else {
console.error('[Unknown error type]', error);
}
}
🛠 Bonus: Extract a Utility
If you want to centralize your logic:
typescript
function handleError(error: unknown) {
if (error instanceof DefinedError) {
console.warn(`[Handled] ${error.code}: ${error.message}`);
} else if (error instanceof UnexpectedError) {
console.error(`[Unexpected] ${error.message}`);
} else {
console.error('[Unknown error]', error);
}
}
Then just:
typescript
try {
await riskyOperation();
} catch (err) {
handleError(err);
}
✅ Benefits of This Pattern
- Cleaner code – separate concerns.
- Better user experience – friendly error messages.
- Monitoring-ready – log only what matters.
- Future-proof – extend with more custom error types if needed.
📦 Summary
With just a few lines of setup, you’ve now got:
- A scalable way to distinguish expected vs unexpected errors.
- A clear pattern for all async logic.
- Confidence in how your app reacts when things break.