Presentation Layer Error Handling Decorator: How It Works in the TypeScript Dummy Apps

The withErrorHandling decorator wraps controller methods in a unified try/catch block to ensure all errors—whether Zod validation failures or runtime exceptions—return a consistent JSON envelope with appropriate status codes.

The castrozan/tcc repository implements a robust error handling strategy across its dummy applications (professionals and equipments) using a TypeScript decorator that intercepts exceptions at the presentation layer. This pattern centralizes error management, allowing controllers to focus strictly on request handling and business logic delegation while guaranteeing standardized API responses.

Decorator Implementation in handleErrors.ts

The core logic resides in professionals-dummy-app/src/presentation/decorators/handleErrors.ts (with an identical copy in the equipments app). The withErrorHandling function conforms to the TypeScript decorator signature for method decorators, accepting the target object, property key, and property descriptor.

export function withErrorHandling(
    targetOrFn: object,
    propertyKey?: string,
    descriptor?: PropertyDescriptor
): PropertyDescriptor {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: unknown[]): Promise<object> {
        try {
            return await originalMethod.apply(this, args);
        } catch (error) {
            console.error(`Error in ${targetOrFn.constructor.name}.${propertyKey}:`, error);

            if (error instanceof z.ZodError) {
                // Validation errors from Zod schemas
                return {
                    success: false,
                    message: 'Validation failed',
                    errors: error.errors
                };
            }

            // Generic runtime errors
            return {
                success: false,
                message: error.message || 'An unexpected error occurred',
                statusCode: error.statusCode || 500
            };
        }
    };
    return descriptor;
}

The decorator mutates the descriptor.value to inject a wrapper function that preserves the original method's context via apply(this, args). This ensures the controller instance (this) remains accessible within the wrapped method.

Error Classification and Response Shapes

The presentation layer error handling decorator distinguishes between two primary failure categories, returning distinct JSON structures for each.

Zod Validation Errors

When input validation fails against a Zod schema (thrown by this.getValidatedData() in controllers), the decorator catches the z.ZodError and returns a structured response containing the specific validation issues:

{
    success: false,
    message: 'Validation failed',
    errors: error.errors  // Array of ZodIssue objects
}

Generic Runtime Errors

For all other exceptions—including domain errors from use cases or unexpected runtime failures—the decorator falls back to a generic envelope that extracts the error message and optional HTTP status code:

{
    success: false,
    message: error.message || 'An unexpected error occurred',
    statusCode: error.statusCode || 500
}

Real-World Usage in Controllers

In professionals-dummy-app/src/presentation/controllers/professionals/UpdateProfessionalController.ts, the decorator is applied directly above the handle method using the @withErrorHandling syntax. This controller extends OpenAPIRoute and uses Zod for request validation.

export class UpdateProfessionalController extends OpenAPIRoute {
    // OpenAPI schema configuration omitted for brevity

    @withErrorHandling
    async handle(): Promise<object> {
        const data = await this.getValidatedData();  // May throw ZodError
        const { id, name, role, bio, imageUrl, hierarchy } = data.body;

        const useCase = new UpdateProfessionalUseCase(professionalRepository);
        const professional = await useCase.execute({ id, name, role, bio, imageUrl, hierarchy });

        return {
            success: true,
            result: {
                id: professional.id,
                name: professional.name,
                // ... additional fields
            }
        };
    }
}

When the HTTP request triggers handle(), the decorator intercepts exceptions from three potential sources: schema validation, use case execution, or unexpected runtime errors. The client always receives a predictable JSON structure regardless of where the failure originated.

Architectural Benefits of the Presentation Layer Error Handling Decorator

Separation of Concerns: By extracting error management into a reusable decorator, controllers remain focused on request parsing, use case invocation, and response shaping. The decorator handles logging, error classification, and HTTP status mapping independently.

Cross-Cutting Concern Implementation: This pattern represents Aspect-Oriented Programming (AOP) in TypeScript. The error handling concern cuts across all API routes, and the decorator provides a declarative way to apply it consistently without code duplication.

Contract Consistency: Every endpoint adheres to the same error-response contract ({ success, message?, statusCode?, errors? }), simplifying frontend error handling and automated API documentation generation.

Extensibility: Additional error classifications—such as custom domain errors or authentication failures—can be added to the catch block in handleErrors.ts without modifying individual controller files.

Implementing the Decorator in New Controllers

To apply the error handling decorator to a new controller, import withErrorHandling from the decorators barrel file and annotate the handle method:

import { withErrorHandling } from 'presentation/decorators';
import { z } from 'zod';
import { SomeUseCase } from 'application/use-cases/some';

export class CreateSomethingController extends OpenAPIRoute {
    schema = { /* OpenAPI schema with Zod validation */ };

    @withErrorHandling
    async handle() {
        const data = await this.getValidatedData();  // throws ZodError on invalid input
        const useCase = new SomeUseCase();
        const result = await useCase.execute(data.body);
        return { success: true, result };
    }
}

The decorator ensures that any thrown exception—whether from Zod validation or the use case layer—returns a properly formatted error response to the client.

Summary

  • The withErrorHandling decorator in professionals-dummy-app/src/presentation/decorators/handleErrors.ts wraps async controller methods to centralize exception handling.
  • It distinguishes between Zod validation errors (returning detailed validation arrays) and generic runtime errors (returning message and status code).
  • Controllers apply the decorator using @withErrorHandling to ensure consistent JSON error envelopes across all OpenAPI routes.
  • This pattern implements Aspect-Oriented Programming principles, keeping controllers thin and error handling logic maintainable in a single location.
  • The same implementation exists in both the professionals and equipments dummy applications for architectural consistency.

Frequently Asked Questions

What is the difference between how the decorator handles Zod errors versus runtime errors?

The decorator checks if the caught error is an instance of z.ZodError. For validation failures, it returns { success: false, message: 'Validation failed', errors: error.errors } with a 200 status (implied) but explicit failure flag. For runtime errors, it returns { success: false, message: error.message, statusCode: error.statusCode || 500 }, defaulting to HTTP 500 when no specific status is attached to the error object.

Can I use the withErrorHandling decorator on non-async methods?

The current implementation in handleErrors.ts explicitly wraps the method with an async wrapper: descriptor.value = async function (...). While you can apply the decorator to synchronous methods, the wrapper will still return a Promise. To avoid potential issues, the decorator should only be applied to async controller methods designed to return Promises.

How do I add support for custom domain errors in the decorator?

Open professionals-dummy-app/src/presentation/decorators/handleErrors.ts and add additional instanceof checks within the catch block before the generic fallback. For example, check for a custom DomainError class to return specific status codes or message formats, then return a specialized response object before the final generic return statement.

Where is the error handling decorator exported from?

The decorator is exported from the barrel file at professionals-dummy-app/src/presentation/decorators/index.ts, allowing you to import it via import { withErrorHandling } from 'presentation/decorators'. The same export structure exists in the equipments dummy app at equipments-dummy-app/src/presentation/decorators/index.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →