Functions

Exception

Python-inspired exception handling with chaining and notes

The Exception type provides Python-inspired exception handling with advanced features like exception chaining, contextual notes, and exception groups.

Why Use Exception?

Traditional JavaScript error handling has limitations:

  • No error chaining: Can't track the original cause of an error
  • Lost context: Error messages lose contextual information as errors propagate
  • No grouping: Can't represent multiple related errors together

Exception solves these problems with Python-inspired features:

  • Exception chaining - Track the root cause with .from()
  • Contextual notes - Add debugging context with .addNote()
  • Exception groups - Combine multiple related errors with .group()
  • Namespaced exceptions - Organize errors by domain
  • Type-safe comparison - Check errors with .is()

Basic Usage

Creating Exceptions

Use the Exception namespace:

import { Exception } from '@deessejs/functions';

// Simple exception
const error = Exception({
  name: "ValidationError",
  message: "Email is required",
});

// With additional data
const error2 = Exception({
  name: "DatabaseError",
  message: "Connection failed",
  data: { host: "localhost", port: 5432 },
});

// With namespace and code
const error3 = Exception({
  name: "ConnectionFailed",
  namespace: "database",
  code: "DB_001",
  message: "Failed to connect to database",
});

Built-in Exception Helpers

Common exception types are built-in:

import { Exception } from '@deessejs/functions';

// Internal server error
Exception.internal("Unexpected error occurred");

// Database error
Exception.database("Failed to connect to database");

// Network error
Exception.network("Connection refused");

// Timeout
Exception.timeout("fetchUsers", 5000);
// { name: "TimeoutError", message: "Operation 'fetchUsers' timed out after 5000ms" }

// Not implemented
Exception.notImplemented("deleteUser");
// { name: "NotImplementedError", message: "Feature 'deleteUser' is not implemented" }

// Validation
Exception.validation("Email is invalid", { field: "email" });

// Not found
Exception.notFound("123", "User");
// { name: "NotFound", message: "User with ID '123' was not found" }

// Unauthorized
Exception.unauthorized("Invalid credentials");

// Forbidden
Exception.forbidden("Insufficient permissions");

// Conflict
Exception.conflict("Email already exists", { email: "test@example.com" });

Exception Chaining

Track the root cause of errors with .from():

import { Exception } from '@deessejs/functions';

async function fetchUser(id: string) {
  try {
    const response = await fetch(`/api/users/${id}`);
    return await response.json();
  } catch (error) {
    // Chain the network error as the cause
    throw Exception({
      name: "UserFetchFailed",
      message: `Failed to fetch user ${id}`,
    }).from(error);
  }
}

// The exception now has a `cause` property
// error.cause is the original network error

Checking Causes

Check if an exception was caused by another:

try {
  await fetchUser("123");
} catch (error) {
  if (error.cause) {
    console.log("Caused by:", error.cause.message);
  }
}

Contextual Notes

Add debugging context with .addNote():

import { Exception } from '@deessejs/functions';

const error = Exception({
  name: "PaymentFailed",
  message: "Payment processing failed",
})
  .addNote("User ID: 123")
  .addNote("Amount: $99.99")
  .addNote("Payment method: credit_card")
  .addNote("Timestamp: 2025-01-15T10:30:00Z");

// error.notes is an array of all notes
console.log(error.notes);
// [
//   "User ID: 123",
//   "Amount: $99.99",
//   "Payment method: credit_card",
//   "Timestamp: 2025-01-15T10:30:00Z"
// ]

Notes for Debugging

Use notes to trace execution flow:

async function processPayment(userId: string, amount: number) {
  try {
    const user = await validateUser(userId);

    if (!user.hasPaymentMethod) {
      throw Exception({
        name: "PaymentFailed",
        message: "No payment method on file",
      })
        .addNote(`User ID: ${userId}`)
        .addNote(`Attempted amount: $${amount}`)
        .addNote("User state: active");
    }

    // ... process payment
  } catch (error) {
    // Add note even if exception already exists
    if (error instanceof Error) {
      error.addNote(`Processing failed at step: payment`);
    }
    throw error;
  }
}

Exception Groups

Combine multiple related errors with Exception.group():

import { Exception } from '@deessejs/functions';

async function fetchMultipleUsers(ids: string[]) {
  const errors: Exception[] = [];

  for (const id of ids) {
    try {
      await fetchUser(id);
    } catch (error) {
      errors.push(
        Exception({
          name: "UserFetchFailed",
          message: `Failed to fetch user ${id}`,
        }).from(error)
      );
    }
  }

  if (errors.length > 0) {
    throw Exception.group("MultipleUsersFetchFailed", errors);
  }
}

// Usage
try {
  await fetchMultipleUsers(["123", "456", "789"]);
} catch (error) {
  // error is an ExceptionGroup
  // error.name === "MultipleUsersFetchFailed"
  // error.exceptions contains all individual errors
}

Accessing Grouped Errors

try {
  await fetchMultipleUsers(["123", "456", "789"]);
} catch (error) {
  if (error.exceptions) {
    for (const exc of error.exceptions) {
      console.log(`- ${exc.name}: ${exc.message}`);
    }
  }
}

Namespaced Exceptions

Organize exceptions by domain with exceptionSpace():

import { exceptionSpace } from '@deessejs/functions/errors';

// Create a namespace for database-related exceptions
const Database = exceptionSpace({
  name: "database",
  code: "DB",
  severity: "error",
});

// Define exceptions in the namespace
const ConnectionError = Database.define({
  name: "ConnectionFailed",
  code: "001",
  message: "Failed to connect to database",
});

const QueryError = Database.define({
  name: "QueryFailed",
  code: "002",
  message: "Query execution failed",
});

// Usage
throw ConnectionError({
  message: "Connection timeout",
  data: { host: "localhost", port: 5432 },
});

Namespace Benefits

Namespaces provide:

  • Organization - Group related exceptions
  • Default codes - Auto-generate error codes
  • Default severity - Set severity for all exceptions
  • Type safety - Distinct types for different domains

Exception Comparison

Check exceptions by name, namespace, or instance:

import { Exception } from '@deessejs/functions';

const error = Exception({
  name: "DatabaseError",
  namespace: "database",
  message: "Connection failed",
});

// Check by name
error.is("DatabaseError");  // true
error.is("NetworkError");   // false

// Check by namespace
error.is("database");       // true
error.is("network");        // false

// Check by instance
const otherError = Exception({ name: "DatabaseError" });
error.is(otherError);       // true

Using is() for Error Handling

try {
  await riskyOperation();
} catch (error) {
  if (error.is("database")) {
    // Handle all database errors
    console.log("Database error:", error.message);
  } else if (error.is("network")) {
    // Handle all network errors
    console.log("Network error:", error.message);
  } else {
    // Handle unknown errors
    console.log("Unknown error:", error.message);
  }
}

Working with Result

Use exceptions with Result types:

import { Exception, Result, tryCatch } from '@deessejs/functions';

function parseUser(json: string): Result<User, Exception> {
  return tryCatch(() => {
    const data = JSON.parse(json);
    return userSchema.parse(data);
  });
}

// Usage
const result = parseUser(invalidJson);

if (result.isFailure()) {
  const error = result.error;
  console.log(`Error: ${error.name}`);
  console.log(`Message: ${error.message}`);
  console.log(`Cause:`, error.cause);
}

Working with Outcome

Use exceptions with Outcome types:

import { Exception, Outcome } from '@deessejs/functions';

async function login(email: string, password: string): PromiseOutcome<User, Cause, Exception> {
  try {
    const user = await db.users.findByEmail(email);
    if (!user) {
      return Outcome.failure(Cause.notFound(email, "User"));
    }

    if (!await verifyPassword(password, user.passwordHash)) {
      return Outcome.failure(Cause.unauthorized("Invalid password"));
    }

    return Outcome.success(user);

  } catch (error) {
    // System error - use Exception
    return Outcome.exception(
      Exception.database("Failed to authenticate user").from(error)
    );
  }
}

Advanced Patterns

Retry with Exception Chaining

import { Exception, retry, Schedule } from '@deessejs/functions';

async function fetchWithRetry(url: string) {
  const lastError = await retry(
    async () => {
      const response = await fetch(url);
      if (!response.ok) {
        throw Exception.network(`HTTP ${response.status}`);
      }
      return response.json();
    },
    Schedule.exponential(1000).attempts(3)
  );

  // If retry fails, chain the last error
  throw Exception({
    name: "FetchFailed",
    message: `Failed to fetch ${url} after retries`,
  }).from(lastError);
}

Exception Enrichment

Add context as exceptions propagate:

import { Exception } from '@deessejs/functions';

async function processOrder(orderId: string) {
  try {
    const order = await fetchOrder(orderId);
    const user = await fetchUser(order.userId);
    const payment = await processPayment(order);

    return { order, user, payment };
  } catch (error) {
    // Enrich exception with order context
    throw Exception({
      name: "OrderProcessingFailed",
      message: `Failed to process order ${orderId}`,
    })
      .from(error)
      .addNote(`Order ID: ${orderId}`)
      .addNote(`User ID: ${error.userId || "unknown"}`)
      .addNote(`Timestamp: ${new Date().toISOString()}`);
  }
}

Conditional Exception Creation

import { Exception } from '@deessejs/functions';

function validateEmail(email: string): void {
  const errors = [
    !email.includes('@') && "Missing @ symbol",
    !email.includes('.') && "Missing domain",
    email.length < 5 && "Email too short",
  ].filter(Boolean) as string[];

  if (errors.length > 0) {
    throw Exception({
      name: "ValidationError",
      message: "Email validation failed",
    }).addNote(`Email: ${email}`).addNote(`Errors: ${errors.join(", ")}`);
  }
}

Best Practices

1. Use Descriptive Names

// ✅ Good - specific and clear
Exception({ name: "UserNotFound", message: "..." });
Exception({ name: "PaymentDeclined", message: "..." });

// ❌ Bad - vague
Exception({ name: "Error", message: "..." });
Exception({ name: "Failed", message: "..." });

2. Add Helpful Data

// ✅ Good - includes context
Exception({
  name: "ValidationError",
  message: "Invalid email format",
  data: { email: "invalid", field: "email" },
});

// ❌ Bad - no context
Exception({ name: "ValidationError", message: "Invalid data" });

3. Use Chaining for Root Causes

// ✅ Good - preserves root cause
throw Exception({
  name: "UserFetchFailed",
  message: "Failed to fetch user",
}).from(originalNetworkError);

// ❌ Bad - loses root cause
throw Exception({
  name: "UserFetchFailed",
  message: originalNetworkError.message,
});

4. Add Notes for Debugging

// ✅ Good - notes help debugging
Exception({
  name: "PaymentFailed",
  message: "Payment processing failed",
})
  .addNote(`User ID: ${userId}`)
  .addNote(`Amount: $${amount}`)
  .addNote(`Retry count: ${retryCount}`);

// ❌ Bad - no debugging context
Exception({
  name: "PaymentFailed",
  message: "Failed",
});

5. Use Namespaces for Organization

// ✅ Good - organized by domain
const Database = exceptionSpace({ name: "database", severity: "error" });
const API = exceptionSpace({ name: "api", severity: "error" });

Database.define({ name: "ConnectionFailed" });
API.define({ name: "RateLimitExceeded" });

// ❌ Bad - flat structure
Exception({ name: "DatabaseConnectionFailed", namespace: "database" });
Exception({ name: "APIRateLimitExceeded", namespace: "api" });

Exception vs Error

When to use Exception vs native Error:

// ✅ Use Exception for business logic errors
throw Exception.validation("Email is required");
throw Exception.notFound("123", "User");
throw Exception.conflict("Email already exists");

// ✅ Use Error for unexpected system errors
throw new Error("Unexpected type");
throw new TypeError("Expected string");
throw new ReferenceError("Variable not defined");

Type Signatures

interface Exception {
  name: string;
  message: string;
  namespace?: string;
  code?: string;
  data?: Record<string, any>;
  cause?: Error | Exception;
  notes?: string[];

  from(cause: Error | Exception): this;
  addNote(note: string): this;
  is(nameOrNamespace: string | Exception): boolean;
}

function Exception(config: {
  name: string;
  message: string;
  namespace?: string;
  code?: string;
  data?: Record<string, any>;
}): Exception;

See Also

  • Result - For success/failure without exceptions
  • Outcome - For distinguishing failures from exceptions
  • Try - For wrapping code that might throw
  • Async Result - Async version of Result

On this page