API Exception Mapping and Validation Strategy

Explains the design philosophy behind CotomyApiException, HTTP status-based validation handling, and why async/await based exception flow improves clarity in business applications.

This note continues from Inheritance and Composition in Business Application Design , Form AJAX Standardization , and Page Lifecycle Coordination .

Introduction

API failure handling is one of those areas where small stylistic choices become large operational costs. On small screens, almost any style can work. On long-lived business systems, error handling style directly changes readability, recovery behavior, and maintenance speed.

This article explains why I prefer exception-oriented API handling in Cotomy, how validation should be treated as an HTTP contract, and why async/await made this approach more natural in everyday business code.

JavaScript Error Handling Before async/await

In JavaScript, async/await arrived in ECMAScript 2017. In Node.js, it became officially supported in Node 7.6 in 2017.

Before that, most asynchronous code around HTTP calls relied on Promise chains. Since fetch itself is Promise-based, code often looked like this:

fetch("/api/users")
  .then(r => r.json())
  .then(data => render(data))
  .catch(e => handleError(e));

This was not wrong. It was the normal style at the time.

Why Promise Chains Felt Unnatural

The problem was not syntax preference. The problem was flow structure.

Promise chains tend to separate steps into fragmented blocks. Success logic and failure logic are connected by conventions, but visually they are split across chained callbacks. In business screens where requests, validation, and side effects are mixed, this fragmentation increases mental load.

For many practical cases, Promise chains made error flow feel like a transport detail rather than a first-class control path.

The Arrival of async/await

With async/await, the same fetch flow became structurally closer to synchronous code:

try {
    const response = await fetch("/api/users");
    const data = await response.json();
    render(data);
}
catch (error) {
    handleError(error);
}

This is why the shift mattered in practice. Promise chains can split structure, while try/catch keeps success and failure in one readable block. In business processing, exception-oriented flow is usually easier to follow during implementation and debugging.

Cotomy’s Exception-Oriented API Handling

By default, the fetch API does not throw exceptions for HTTP errors such as 400 or 500 responses. Cotomy intentionally converts those responses into exceptions so that HTTP failures participate in the same control flow as runtime errors.

Cotomy treats HTTP failures as exceptions on the client side. API failures are converted into CotomyApiException-derived types, and the caller handles them through try/catch.

This policy is intentionally simple.

The flow stays linear. The success path and failure path are explicit. Promise chain branching is avoided. The handler reads like a business transaction: submit, parse, branch by failure type if needed.

HTTP Status Codes as Validation Contracts

Validation is not just a UI concern. It is part of the HTTP response contract.

I prefer defining status meanings clearly and treating them as stable operational agreements between server and client:

Status Meaning
200 Success
201 Created
400 Invalid request format or missing required input
409 Conflict
422 Validation error
500 Server error

Validation is not always an exceptional situation from a domain perspective. However, on the client side it is often handled as an exception-like control path in order to keep UI flow consistent.

CotomyApiException Class Structure

The exception hierarchy in Cotomy is designed to mirror HTTP semantics and make typed branching straightforward in UI code.

classDiagram
Error <|-- CotomyApiException
CotomyApiException <|-- CotomyHttpClientError
CotomyApiException <|-- CotomyHttpServerError
CotomyHttpClientError <|-- CotomyUnauthorizedException
CotomyHttpClientError <|-- CotomyForbiddenException
CotomyHttpClientError <|-- CotomyNotFoundException
CotomyHttpClientError <|-- CotomyConflictException
CotomyHttpClientError <|-- CotomyRequestInvalidException
CotomyHttpClientError <|-- CotomyTooManyRequestsException

By splitting exception classes by HTTP intent, UI code can branch by type without inventing its own parallel error taxonomy. This keeps HTTP specification and application structure aligned.

The practical boundary in Cotomy is this: unexpected runtime failures remain generic Error-level failures, while HTTP contract failures are grouped under CotomyApiException as application-level exceptions.

This is also where naming history matters. CotomyHttpClientError is still part of the CotomyApiException hierarchy and works as a fallback for unmapped 4xx cases. So in behavior it belongs to the API exception flow, even if the class name itself uses Error.

In hindsight, some names could have been more explicit. But those names are already part of the public API surface, so I keep them in the current major line for compatibility and revisit naming consistency in the next major version.

The current Cotomy code also keeps non-HTTP failures separate, for example response JSON parse failure and invalid body input handling. Those are not status-mapped API failures, and separating them keeps the contract boundary explicit.

Validation and API Failure Sequences

At runtime, form submit behavior can be described as one sequence with clear status-dependent branches:

sequenceDiagram
Browser->>CotomyForm: submit()
CotomyForm->>CotomyApi: POST /api/entity
CotomyApi->>Server: HTTP Request
alt Success
    Server-->>CotomyApi: 200 OK
    CotomyApi-->>CotomyForm: Response
else Validation Error
    Server-->>CotomyApi: 422 Validation Error
    CotomyApi-->>CotomyForm: throw CotomyApiException
    CotomyForm-->>UI: cotomy:submitfailed event
else Server Error
    Server-->>CotomyApi: 500
    CotomyApi-->>CotomyForm: throw CotomyHttpServerError
end

And a typical client handler looks like this:

try {
    const response = await api.submitAsync({
        method: "POST",
        action: "/api/users",
        body: formData
    });

    const result = await response.objectAsync();

    render(result);
}
catch (error) {

    if (error instanceof CotomyRequestInvalidException) {
        showValidation(error.response);
    }
    else if (error instanceof CotomyConflictException) {
        showConflictMessage();
    }
    else {
        showUnexpectedError(error);
    }

}

This reflects Cotomy’s broader design: server side defines strict HTTP contracts, and client side handles validation failures, API failures, and server errors in one consistent exception flow. The result is simpler UI code and more predictable error handling.

Conclusion

Promise chains were used for historical reasons, and they were the practical option before async/await became standard. But async/await made exception structure far more natural for business application flows.

Cotomy intentionally treats HTTP failures as exceptions, maps them into explicit exception classes, and expects validation to be designed as an HTTP contract. When server contracts are strict, client design becomes simpler, clearer, and more maintainable.

Design Series

This article is part of the Cotomy Design Series, which explores architectural decisions behind the framework.

Series articles: CotomyElement Boundary , Page Lifecycle Coordination , Form AJAX Standardization , Inheritance and Composition in Business Application Design , API Exception Mapping and Validation Strategy, and Why Modern Developers Avoid Inheritance .

Next article: Why Modern Developers Avoid Inheritance

Learn Cotomy

Cotomy is a DOM-first UI runtime for long-lived business applications.