Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. FrameworksSeparator
  3. TypescriptSeparator
  4. Errors And Responses

Errors & Responses

Putnami provides typed HTTP exceptions and response factories for building consistent APIs with proper error handling.

HTTP exceptions

Built-in exceptions

Import and throw HTTP exceptions from @putnami/runtime:

import {
  BadRequestException,      // 400
  UnauthorizedException,    // 401
  ForbiddenException,       // 403
  NotFoundException,        // 404
  MethodNotAllowedException, // 405
  ConflictException,        // 409
  PayloadTooLargeException, // 413
  UnprocessableEntityException, // 422
  TooManyRequestsException, // 429
  InternalServerErrorException, // 500
  NotImplementedException,  // 501
  ServiceUnavailableException, // 503
} from '@putnami/runtime';

Throwing exceptions

import { endpoint, Uuid } from '@putnami/application';
import { NotFoundException, ForbiddenException } from '@putnami/runtime';

export default endpoint()
  .params({ id: Uuid })
  .handle(async (ctx) => {
    const user = await findUser(ctx.params.id);

    if (!user) {
      throw new NotFoundException(`User ${ctx.params.id} not found`);
    }

    if (!canAccess(ctx, user)) {
      throw new ForbiddenException('Access denied');
    }

    return user;
  });

Exception with details

throw new BadRequestException('Validation failed', {
  fields: {
    email: 'Invalid email format',
    password: 'Password too short',
  },
});

Custom exceptions

Create custom exceptions by extending HttpException:

import { HttpException } from '@putnami/runtime';

export class RateLimitException extends HttpException {
  constructor(retryAfter: number) {
    super(429, `Rate limit exceeded. Retry after ${retryAfter} seconds`);
    this.retryAfter = retryAfter;
  }

  retryAfter: number;
}

// Usage
throw new RateLimitException(60);

Validation

The endpoint() builder validates inputs automatically. Invalid requests are rejected before your handler runs:

import { endpoint, Email, MinLength } from '@putnami/application';

export default endpoint()
  .body({ email: Email, password: MinLength(8) })
  .handle(async (ctx) => {
    const body = await ctx.body();
    // body.email and body.password are validated
  });

Context throw

Use ctx.throw() for quick error responses:

import { endpoint } from '@putnami/application';

export default endpoint((ctx) => {
  const id = ctx.queryParams().get('id');

  if (!id) {
    ctx.throw(400, 'ID is required');
  }

  return { id };
});

Response factories

Creating responses

import { endpoint, HttpResponse, json } from '@putnami/application';

// JSON response
export default endpoint(() => {
  return json({ message: 'Hello' });
});

// JSON with status
export default endpoint(() => {
  return json({ created: true }, { status: 201 });
});

// Redirect
export default endpoint(() => {
  return HttpResponse.redirect('/new-location');
});

// Redirect with status
export default endpoint(() => {
  return HttpResponse.redirect('/permanent', 301);
});

Error response factories

import { badRequest, unauthorized, forbidden, notFound, internalServerError } from '@putnami/application';

// 400 Bad Request
badRequest('Invalid input');

// 401 Unauthorized
unauthorized('Authentication required');

// 403 Forbidden
forbidden('Access denied');

// 404 Not Found
notFound('Resource not found');

// 500 Internal Server Error
internalServerError('Something went wrong');

Custom headers

import { endpoint, json } from '@putnami/application';

export default endpoint(() => {
  return json({ data: 'value' })
    .setHeader('X-Request-Id', crypto.randomUUID())
    .setHeader('Cache-Control', 'max-age=3600');
});

Multiple headers with same name

import { endpoint, json } from '@putnami/application';

export default endpoint(() => {
  return json({ ok: true })
    .pushHeader('Set-Cookie', 'session=abc; HttpOnly')
    .pushHeader('Set-Cookie', 'tracking=xyz');
});

Custom response

import { endpoint, HttpResponse } from '@putnami/application';

export default endpoint(() => {
  return new HttpResponse(new Response('Plain text', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    },
  }));
});

File download

import { endpoint, HttpResponse } from '@putnami/application';

export default endpoint(async () => {
  const file = Bun.file('report.pdf');
  const content = await file.arrayBuffer();

  return new HttpResponse(new Response(content, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="report.pdf"',
      'Content-Length': String(content.byteLength),
    },
  }));
});

Streaming response

import { endpoint, HttpResponse } from '@putnami/application';

export default endpoint(() => {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        controller.enqueue(`data: ${i}\n\n`);
        await new Promise(r => setTimeout(r, 1000));
      }
      controller.close();
    },
  });

  return new HttpResponse(new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    },
  }));
});

Error handling patterns

Global error handler

import type { HttpMiddleware } from '@putnami/application';
import { HttpException, ValidationException } from '@putnami/runtime';
import { json, internalServerError } from '@putnami/application';

export const errorHandler: HttpMiddleware = async (ctx, next) => {
  try {
    return await next();
  } catch (error) {
    // Known HTTP exceptions
    if (error instanceof HttpException) {
      return json(
        { error: error.message, code: error.status },
        { status: error.status }
      );
    }

    // Validation errors
    if (error instanceof ValidationException) {
      return json(
        { error: 'Validation failed', details: error.errors },
        { status: 422 }
      );
    }

    // Log unexpected errors
    console.error('Unexpected error:', error);

    return internalServerError('An unexpected error occurred');
  }
};

Domain-specific errors

import { endpoint, json } from '@putnami/application';

// Define domain errors
export class InsufficientFundsError extends Error {
  constructor(public available: number, public required: number) {
    super(`Insufficient funds: ${available} available, ${required} required`);
  }
}

// Handle in route
export default endpoint(async () => {
  try {
    await processPayment(amount);
    return { success: true };
  } catch (error) {
    if (error instanceof InsufficientFundsError) {
      return json(
        {
          error: 'Insufficient funds',
          available: error.available,
          required: error.required,
        },
        { status: 402 }
      );
    }
    throw error;
  }
});

Error aggregation

import { endpoint } from '@putnami/application';
import { AggregateException } from '@putnami/runtime';

export default endpoint(async (ctx) => {
  const body = await ctx.body();
  const errors: Error[] = [];

  // Collect multiple errors
  if (!isValidEmail(body.email)) {
    errors.push(new Error('Invalid email'));
  }

  if (!isValidPassword(body.password)) {
    errors.push(new Error('Invalid password'));
  }

  if (errors.length > 0) {
    throw new AggregateException(errors);
  }

  // Process valid input...
});

Response status codes

Success codes

import { endpoint, HttpResponse, json } from '@putnami/application';

// 200 OK (default)
return { data: 'value' };

// 201 Created
return json({ id: newId }, { status: 201 });

// 204 No Content
export default endpoint(async () => {
  await deleteResource();
  return undefined;
});

Redirect codes

import { HttpResponse } from '@putnami/application';

// 301 Moved Permanently
HttpResponse.redirect('/new-url', 301);

// 302 Found (default)
HttpResponse.redirect('/new-url');

// 303 See Other
HttpResponse.redirect('/new-url', 303);

// 307 Temporary Redirect
HttpResponse.redirect('/new-url', 307);

Client error codes

// 400 Bad Request
throw new BadRequestException('Invalid input');

// 401 Unauthorized
throw new UnauthorizedException('Please log in');

// 403 Forbidden
throw new ForbiddenException('Not allowed');

// 404 Not Found
throw new NotFoundException('Resource not found');

// 409 Conflict
throw new ConflictException('Resource already exists');

// 422 Unprocessable Entity
throw new ValidationException(errors);

// 429 Too Many Requests
throw new TooManyRequestsException('Rate limit exceeded');

Server error codes

// 500 Internal Server Error
throw new InternalServerErrorException('Server error');

// 501 Not Implemented
throw new NotImplementedException('Feature not available');

// 503 Service Unavailable
throw new ServiceUnavailableException('Service temporarily unavailable');

Related guides

  • API
  • HTTP & Middleware

On this page

  • Errors & Responses
  • HTTP exceptions
  • Built-in exceptions
  • Throwing exceptions
  • Exception with details
  • Custom exceptions
  • Validation
  • Context throw
  • Response factories
  • Creating responses
  • Error response factories
  • Custom headers
  • Multiple headers with same name
  • Custom response
  • File download
  • Streaming response
  • Error handling patterns
  • Global error handler
  • Domain-specific errors
  • Error aggregation
  • Response status codes
  • Success codes
  • Redirect codes
  • Client error codes
  • Server error codes
  • Related guides