Skip to main content

Overview

StatusFlow provides a hierarchy of exception classes that make error handling in Express applications cleaner and more expressive. Instead of manually managing status codes and messages, you can throw semantic exceptions that are automatically handled by the middleware.

HttpException Base Class

All exception classes inherit from the base HttpException class, which extends the native JavaScript Error class.

Class Definition

class HttpException extends Error {
  public readonly status: number;
  public readonly details?: unknown;

  constructor(status: number, message: string, details?: unknown) {
    super(message);
    this.status = status;
    this.details = details;
    Object.setPrototypeOf(this, new.target.prototype);
    if (typeof (Error as any).captureStackTrace === 'function') {
      (Error as any).captureStackTrace(this, this.constructor);
    }
  }
}

Properties

status
number
required
The HTTP status code for this exception
message
string
required
The error message (inherited from Error)
details
unknown
Optional additional details about the error (e.g., validation errors, stack traces)

Exception Classes

StatusFlow provides pre-built exception classes for the most common HTTP error scenarios:

BadRequestException (400)

Use when the client sends invalid or malformed data.
import { BadRequestException } from 'status-flow';

// Basic usage
throw new BadRequestException();
// Default message: "Bad Request"

// With custom message
throw new BadRequestException('Invalid email format');

// With details
throw new BadRequestException('Validation failed', {
  errors: [
    { field: 'email', message: 'Must be a valid email' },
    { field: 'age', message: 'Must be at least 18' }
  ]
});
When to use:
  • Invalid request body format
  • Missing required fields
  • Invalid data types or formats
  • Validation errors

UnauthorizedException (401)

Use when authentication is required but not provided or invalid.
import { UnauthorizedException } from 'status-flow';

// Basic usage
throw new UnauthorizedException();
// Default message: "Unauthorized"

// With custom message
throw new UnauthorizedException('Invalid authentication token');

// With details
throw new UnauthorizedException('Token expired', {
  expiredAt: '2026-03-07T10:00:00Z',
  tokenId: 'abc123'
});
When to use:
  • Missing authentication credentials
  • Invalid or expired tokens
  • Failed login attempts
  • Invalid API keys

ForbiddenException (403)

Use when the user is authenticated but doesn’t have permission for the requested resource.
import { ForbiddenException } from 'status-flow';

// Basic usage
throw new ForbiddenException();
// Default message: "Forbidden"

// With custom message
throw new ForbiddenException('Insufficient permissions to access this resource');

// With details
throw new ForbiddenException('Admin access required', {
  userRole: 'user',
  requiredRole: 'admin',
  resource: '/api/admin/users'
});
When to use:
  • User lacks required permissions
  • Resource access is restricted
  • Account is suspended or blocked
  • Rate limit exceeded

NotFoundException (404)

Use when a requested resource doesn’t exist.
import { NotFoundException } from 'status-flow';

// Basic usage
throw new NotFoundException();
// Default message: "Not Found"

// With custom message
throw new NotFoundException('User not found');

// With details
throw new NotFoundException('Product not found', {
  productId: '12345',
  searchedIn: 'products_table'
});
When to use:
  • Resource ID doesn’t exist
  • Invalid route or endpoint
  • Deleted or archived resources
  • Search returns no results

ConflictException (409)

Use when a request conflicts with the current state of the server.
import { ConflictException } from 'status-flow';

// Basic usage
throw new ConflictException();
// Default message: "Conflict"

// With custom message
throw new ConflictException('Email already registered');

// With details
throw new ConflictException('Username already taken', {
  field: 'username',
  value: 'john_doe',
  existingId: '67890'
});
When to use:
  • Duplicate resource creation
  • Unique constraint violations
  • Version conflicts (optimistic locking)
  • Resource state conflicts

InternalServerErrorException (500)

Use when an unexpected server error occurs.
import { InternalServerErrorException } from 'status-flow';

// Basic usage
throw new InternalServerErrorException();
// Default message: "Internal Server Error"

// With custom message
throw new InternalServerErrorException('Database connection failed');

// With details
throw new InternalServerErrorException('Service unavailable', {
  service: 'payment-processor',
  error: 'Connection timeout',
  retryAfter: 60
});
When to use:
  • Database connection failures
  • External service errors
  • Unexpected exceptions
  • Configuration errors
Avoid exposing sensitive error details in production. Use the details field carefully and sanitize information before sending to clients.

Integration with Express

HTTP exceptions work seamlessly with Express error-handling middleware. The library provides a middleware that automatically catches these exceptions and formats them into proper HTTP responses.

Using the Error Middleware

import express from 'express';
import { httpErrorMiddleware, NotFoundException } from 'status-flow';

const app = express();

// Your routes
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await findUserById(req.params.id);
    
    if (!user) {
      throw new NotFoundException('User not found', {
        userId: req.params.id
      });
    }
    
    res.json(user);
  } catch (error) {
    next(error); // Pass error to middleware
  }
});

// Add error middleware (must be after routes)
app.use(httpErrorMiddleware);

app.listen(3000);
The middleware automatically:
  1. Catches HttpException instances
  2. Extracts the status code and message
  3. Creates a structured JSON response
  4. Handles unknown errors with a 500 response

Response Format

When an exception is thrown, the middleware creates this response structure:
{
  status: number,    // HTTP status code
  message: string,   // Error message
  details?: unknown  // Optional additional details
}
Example response for a NotFoundException:
{
  "status": 404,
  "message": "User not found",
  "details": {
    "userId": "12345"
  }
}

Creating Custom Exceptions

You can create your own exception classes by extending HttpException:
import { HttpException } from 'status-flow';

// Custom 422 Unprocessable Entity exception
export class ValidationException extends HttpException {
  constructor(message = 'Validation failed', details?: unknown) {
    super(422, message, details);
  }
}

// Custom 503 Service Unavailable exception
export class ServiceUnavailableException extends HttpException {
  constructor(message = 'Service temporarily unavailable', details?: unknown) {
    super(503, message, details);
  }
}

// Usage
throw new ValidationException('Invalid user data', {
  errors: validationErrors
});

Practical Examples

API Endpoint with Multiple Exceptions

import { 
  BadRequestException, 
  UnauthorizedException, 
  ForbiddenException,
  NotFoundException 
} from 'status-flow';

app.delete('/users/:id', async (req, res, next) => {
  try {
    // Check authentication
    const token = req.headers.authorization;
    if (!token) {
      throw new UnauthorizedException('Authentication required');
    }
    
    // Verify token
    const user = await verifyToken(token);
    if (!user) {
      throw new UnauthorizedException('Invalid token');
    }
    
    // Validate ID format
    if (!isValidId(req.params.id)) {
      throw new BadRequestException('Invalid user ID format');
    }
    
    // Check if target user exists
    const targetUser = await findUserById(req.params.id);
    if (!targetUser) {
      throw new NotFoundException('User not found');
    }
    
    // Check permissions
    if (user.id !== targetUser.id && user.role !== 'admin') {
      throw new ForbiddenException('Cannot delete other users');
    }
    
    // Perform deletion
    await deleteUser(targetUser.id);
    res.status(204).send();
    
  } catch (error) {
    next(error);
  }
});

Service Layer with Exceptions

import { NotFoundException, ConflictException } from 'status-flow';

class UserService {
  async createUser(email: string, username: string) {
    // Check for existing email
    const existingEmail = await this.findByEmail(email);
    if (existingEmail) {
      throw new ConflictException('Email already registered', {
        field: 'email',
        value: email
      });
    }
    
    // Check for existing username
    const existingUsername = await this.findByUsername(username);
    if (existingUsername) {
      throw new ConflictException('Username already taken', {
        field: 'username',
        value: username
      });
    }
    
    return await this.create({ email, username });
  }
  
  async getUserById(id: string) {
    const user = await this.findById(id);
    if (!user) {
      throw new NotFoundException('User not found', { userId: id });
    }
    return user;
  }
}

Async/Await Error Handling

import { InternalServerErrorException } from 'status-flow';

app.post('/payment', async (req, res, next) => {
  try {
    const payment = await processPayment(req.body);
    res.json(payment);
  } catch (error) {
    if (error instanceof HttpException) {
      // StatusFlow exception - let middleware handle it
      next(error);
    } else {
      // Unknown error - wrap in server error
      next(new InternalServerErrorException('Payment processing failed', {
        originalError: error.message
      }));
    }
  }
});

Comparison with StatusFlow Function

HTTP exceptions and the StatusFlow function serve different purposes:
Best for:
  • Throwing errors in application logic
  • Express error handling
  • Type-safe exception handling
  • Clean, semantic code
if (!user) {
  throw new NotFoundException('User not found');
}
You can use both approaches in the same application. Use exceptions for errors and StatusFlow for success responses.

Best Practices

Use the most specific exception class that matches your scenario. Don’t use BadRequestException for everything.
Default messages like “Not Found” aren’t helpful. Use specific messages like “User not found” or “Product with ID 123 not found”.
Include helpful debugging information in details, but avoid exposing sensitive data or internal implementation details.
Use the same exception classes and format across your entire application for consistency.
Always log InternalServerErrorException with full stack traces for debugging, but don’t send stack traces to clients in production.

Next Steps

StatusFlow Function

Learn about the core StatusFlow function for response generation

Bilingual Responses

Understand the bilingual response system

Build docs developers (and LLMs) love