Error handling in JavaScript is often overlooked until it bites us in production. In modern apps—especially those involving async operations like API calls or file handling—it’s essential to separate the expected errors from the truly unexpected ones.

- Category
- React, Typescript, Electron
- Published
- April 15, 2025
- Author
- Marcus Nguyen
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.
