Back to blog
ReactTypescriptElectron

Error Handling in JavaScript/TypeScript: Defined vs Unexpected Errors

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.

April 15, 2025Marcus Nguyen
Error Handling in JavaScript/TypeScript: Defined vs Unexpected Errors

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.