Handle errors in a type-safe way with TypeScript.

Result Pattern

Instead of throwing, return a result object.

type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { success: false, error: new Error("Division by zero") };
  }
  return { success: true, data: a / b };
}

const result = divide(10, 2);
if (result.success) {
  console.log(result.data); // TypeScript knows data exists
} else {
  console.error(result.error); // TypeScript knows error exists
}

Option/Maybe Pattern

Represent values that might not exist.

type Option<T> = T | null;

function findUser(id: string): Option<User> {
  // Might return null
  return users.find(u => u.id === id) ?? null;
}

const user = findUser("123");
if (user) {
  // TypeScript knows user is not null
  console.log(user.name);
}

Better Option Type

type Some<T> = { kind: "some"; value: T };
type None = { kind: "none" };
type Option<T> = Some<T> | None;

function findUser(id: string): Option<User> {
  const user = users.find(u => u.id === id);
  return user ? { kind: "some", value: user } : { kind: "none" };
}

const result = findUser("123");
if (result.kind === "some") {
  console.log(result.value.name); // TypeScript knows value exists
}

Try-Catch with Types

TypeScript doesn't type catch blocks well, so be explicit.

function riskyOperation(): string {
  // Might throw
  return "result";
}

try {
  const result = riskyOperation();
  // result is string
} catch (error) {
  // error is unknown, not any
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error");
  }
}

Custom Error Classes

Create typed error classes.

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string,
    public value: any
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

class NetworkError extends Error {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message);
    this.name = "NetworkError";
  }
}

type AppError = ValidationError | NetworkError;

function handleError(error: AppError) {
  if (error instanceof ValidationError) {
    console.error(`Validation failed for ${error.field}: ${error.message}`);
  } else if (error instanceof NetworkError) {
    console.error(`Network error ${error.statusCode}: ${error.message}`);
  }
}

Assertion Functions for Errors

function assert(condition: boolean, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

function process(value: string | null) {
  assert(value !== null, "Value cannot be null");
  // TypeScript knows value is string here
  console.log(value.length);
}

Exercises

Exercise 1: Result Pattern

// Create a function that:
// - Takes a URL string
// - Returns Result<string> (the fetched data or error)
// - Use the Result type to handle success/failure

Exercise 2: Option Type

// Create a function 'getProperty' that:
// - Takes an object and a key
// - Returns Option<T> for the property value
// - Returns None if property doesn't exist

Exercise 3: Custom Errors

// Create custom error classes:
// - ParseError
// - NotFoundError
// - PermissionError
// Create a function that throws appropriate errors

Exercise 4: Error Handling Function

// Create a function 'safeExecute' that:
// - Takes a function that might throw
// - Returns Result<T, Error>
// - Catches errors and returns them in Result format

Exercise 5: Error Union

// Create a union type for different error types
// Create a handler that uses type narrowing to handle each error type

Best Practices

  1. Use Result pattern - More type-safe than exceptions
  2. Use Option for nullable values - Explicit about absence
  3. Create custom error classes - Better error handling
  4. Type catch blocks - Don't assume error is Error
  5. Document error conditions - Make them explicit in types

Common Mistakes

  1. Using any for errors
// ❌ Bad
catch (error: any) {
  console.log(error.message); // Might not exist
}

// ✅ Good
catch (error: unknown) {
  if (error instanceof Error) {
    console.log(error.message);
  }
}
  1. Not handling all error cases
// ❌ Missing error handling
function process(data: string) {
  return JSON.parse(data); // Might throw
}

// ✅ Handle errors
function process(data: string): Result<any> {
  try {
    return { success: true, data: JSON.parse(data) };
  } catch (error) {
    return { success: false, error: error as Error };
  }
}
  1. Throwing instead of returning errors
// ❌ Throwing loses type information
function divide(a: number, b: number): number {
  if (b === 0) throw new Error("Division by zero");
  return a / b;
}

// ✅ Return Result
function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { success: false, error: new Error("Division by zero") };
  }
  return { success: true, data: a / b };
}

Next Steps

Congratulations! You've completed the Intermediate level.
Proceed to Advanced Level to learn about advanced type system features.