Skip to content

Error handling ​

Every safe runner on RequestBuilder (run, runAdapter, runArrayAdapter, runPaginatedAdapter, runEmpty, runData, runDataKey, runStatusCode) feeds the response through a single normaliser. The normaliser maps the most common failure modes onto three error classes.

Failure modes ​

SourceThrown as
JSON body with code, error, error_descriptionRequestError
Same envelope plus a nested errors mapValidationError
Non-JSON body on a non-2xx responseRequestError with code -1 and key not_a_json_response
Aborted request (e.g. via autoCancel)RequestAbortedError

401 and 403 responses without a JSON envelope resolve with BaseResponse(null, response) rather than throwing, so authentication redirects can be handled at the call site.

Type guards ​

The package ships predicate helpers so consumers do not need to compare classes manually:

ts
import {
    isRequestAborted,
    isRequestError,
    isUnsanctionedRequest,
    isValidationError
} from '@basmilius/http-client';
  • isRequestError(value) — narrows to RequestError.
  • isValidationError(value) — narrows to ValidationError. Note that ValidationError is a structural sibling of RequestError, so check for it first when both branches matter.
  • isRequestAborted(value) — narrows to RequestAbortedError.
  • isUnsanctionedRequest(value) — true for status codes 401 and 403. Accepts both RequestError instances (reads statusCode) and raw status codes.
ts
import {
    isRequestAborted,
    isRequestError,
    isUnsanctionedRequest,
    isValidationError
} from '@basmilius/http-client';

try {
    await userService.update(user);
} catch (error) {
    if (isRequestAborted(error)) {
        return;
    }

    if (isValidationError(error)) {
        showFieldErrors(error.errors);
        return;
    }

    if (isRequestError(error)) {
        if (isUnsanctionedRequest(error)) {
            redirectToLogin();
            return;
        }

        showToast(error.errorDescription);
        return;
    }

    throw error;
}

Validation errors ​

ValidationError.errors is a recursive map. Nested objects (for example, address.street) appear as nested ValidationError instances under their key. Use errors[field].errorDescription for leaf messages and recurse into errors[field].errors for nested forms.

ts
import { ValidationError } from '@basmilius/http-client';

function flatten(prefix: string, error: ValidationError, into: Record<string, string>): void {
    if (error.errorDescription) {
        into[prefix || error.error] = error.errorDescription;
    }

    for (const key in error.errors) {
        flatten(prefix ? `${prefix}.${key}` : key, error.errors[key], into);
    }
}

Aborting requests ​

RequestBuilder.autoCancel(symbol) registers an in-flight request under a shared identifier. The next request issued with the same identifier aborts the previous one, which surfaces a RequestAbortedError to the awaiter. This is the canonical way to make a search-as-you-type endpoint behave.

ts
import { QueryString } from '@basmilius/http-client';

const SEARCH_TOKEN = Symbol('search');

await this
    .request('/search')
    .method('get')
    .queryString(QueryString.builder()
        .set('q', term))
    .autoCancel(SEARCH_TOKEN)
    .runArrayAdapter(SearchAdapter.parseResult);

For finer control, pass an AbortSignal directly via .signal(controller.signal) and abort it manually.

Network failures ​

fetch itself can reject with TypeError for offline / DNS errors. Those are not wrapped — let them bubble up, or wrap them at the service boundary if you need a single error shape.