Skip to main content

Error Hierarchy

Titan distinguishes between classes (the JavaScript types you can instanceof) and codes (the ErrorCode enum values that classify the underlying error). Most errors are instances of TitanError with a specific ErrorCode; a few have dedicated subclasses.

Class hierarchy

That's the complete class tree.

Errors like "not found" (404), "conflict" (409), "validation" (422) are not separate classes — they are instances of TitanError with code: ErrorCode.NOT_FOUND, ErrorCode.CONFLICT, ErrorCode.VALIDATION_ERROR. Use the Errors namespace to throw them; use e.code === ErrorCode.X to discriminate on the client.

The narrow subclasses — when class identity matters

Three subclasses are dedicated because they need extra fields beyond what TitanError carries:

AuthError (401)

Authentication failure. Carries auth-specific context (provider, reason, hints for the client about how to re-authenticate).

import { AuthError } from '@omnitron-dev/titan/errors';

throw new AuthError({
message: 'session expired',
details: { reason: 'token_expired', refreshable: true },
});

Catch by instanceof AuthError when you want auth-specific handling (e.g. redirect to login).

PermissionError (403)

Authorisation failure — caller is known but lacks the right scope or role. Carries the missing capability for diagnostics.

import { PermissionError } from '@omnitron-dev/titan/errors';

throw new PermissionError({
message: 'users:write scope required',
details: { required: ['users:write'], userScopes: ['users:read'] },
});

RateLimitError (429)

Throttling. Carries the retry-after hint:

import { RateLimitError } from '@omnitron-dev/titan/errors';

throw new RateLimitError({
message: 'too many requests',
details: { retryAfter: 60 }, // seconds
});

AggregateError

Multiple errors batched into one. Useful when a batch operation partially fails:

import { AggregateError, Errors } from '@omnitron-dev/titan/errors';

const errors: TitanError[] = [];
for (const item of batch) {
try {
await this.processItem(item);
} catch (e) {
errors.push(toTitanError(e));
}
}
if (errors.length > 0) {
throw new AggregateError({
message: `${errors.length} items failed`,
errors,
});
}

DomainError

Base for project-specific error classes. Use the defineDomainCodes helper to declare a typed code namespace, then create errors from it:

import { defineDomainCodes, DomainError } from '@omnitron-dev/titan/errors';

const BillingCodes = defineDomainCodes('BILLING', {
CARD_DECLINED: { httpStatus: 402, message: 'Payment declined' },
INSUFFICIENT_BALANCE: { httpStatus: 409, message: 'Insufficient balance' },
FRAUD_DETECTED: { httpStatus: 403, message: 'Fraudulent activity detected' },
});

throw new DomainError({
code: BillingCodes.CARD_DECLINED,
message: 'Card 4242 declined',
details: { last4: '4242', reason: 'do_not_honor' },
});

The framework provides createDomainErrorFactory, createSimpleDomainFactory, isDomainCode, isDomainError, getDomainCode helpers for richer typing — see the source for current signatures.

The wide use of ErrorCode

For everything not in the class tree above, use the appropriate ErrorCode:

StatusCodeThrow via
400BAD_REQUESTErrors.badRequest(...)
401UNAUTHORIZEDErrors.unauthorized(...) or new AuthError(...)
403FORBIDDENErrors.forbidden(...) or new PermissionError(...)
404NOT_FOUNDErrors.notFound(resource, id?)
409CONFLICTErrors.conflict(...) / Errors.alreadyExists(...)
422VALIDATION_ERRORErrors.validation(...)
429RATE_LIMITEDErrors.rateLimit(...) or new RateLimitError(...)
500INTERNAL_ERRORErrors.internal(...) (rare; framework usually wraps)
503SERVICE_UNAVAILABLEErrors.unavailable(...)

The NetronErrors namespace covers transport-specific codes: serviceNotFound, methodNotFound, transportError, timeout, protocolError. They produce TitanError instances with the appropriate code.

Discrimination on the client

Two ways, both supported:

import { TitanError, ErrorCode, AuthError, PermissionError, RateLimitError }
from '@omnitron-dev/titan/errors';

catch (e) {
// By class (works for the four subclasses above + DomainError)
if (e instanceof AuthError) return redirectToLogin();
if (e instanceof PermissionError) return showForbiddenScreen();
if (e instanceof RateLimitError) return backoff(e.details.retryAfter);

// By code (for everything else)
if (e instanceof TitanError) {
switch (e.code) {
case ErrorCode.NOT_FOUND: return show404();
case ErrorCode.CONFLICT: return showConflict(e.details);
case ErrorCode.VALIDATION_ERROR: return showFormErrors(e.details);
}
}

// Unknown — re-throw
throw e;
}

Use class-based dispatch when you have a subclass. Use code-based dispatch for the long tail of TitanError variants.

→ Next: Factories.