Skip to main content

Errors catalog

Per-module catalog of the errors a Titan service may emit, the codes they carry on the wire, and the recommended handling pattern. Useful when designing error-handling middleware or writing retry / fallback logic.

All claims here are verified against module source (greps in packages/titan-*/src for Errors.X(, throw new XError, and class declarations).

Two flavours of error

SourceShapeWire-friendly
Errors.X(...) from @omnitron-dev/titan/errorsTitanError with code: ErrorCode enum
Dedicated subclass per moduleclass FooError extends Error (sometimes TitanError)varies

The framework prefers the Errors namespace — these errors serialise across Netron transports with the class name and code preserved. Bespoke subclasses are reserved for cases where the extra fields (e.g., retryAfter on RateLimitExceededError) make the API clearer.

ErrorCode cheat sheet

The most commonly thrown codes across all modules:

CodeNumericWhen used
BAD_REQUEST400Caller-supplied input invalid
UNAUTHORIZED401Missing or invalid credentials
FORBIDDEN403Authenticated but disallowed
NOT_FOUND404Target identifier doesn't exist
CONFLICT409Resource state contradicts the request
TOO_MANY_REQUESTS429Rate limit / quota exceeded
INTERNAL_ERROR500Unexpected; investigate
NOT_IMPLEMENTED501Feature flag off / variant missing
SERVICE_UNAVAILABLE503Transient — downstream dep failed
REQUEST_TIMEOUT408Operation exceeded configured timeout
VALIDATION_ERROR422Schema validation failed

These behave like HTTP status codes on the wire but are transport-agnostic — the same codes ride TCP / Unix / WS / HTTP.

Aggregate count per module

What each module raises most often:

Official@omnitron-dev/titan-*

Maintained by the Omnitron team. Independent npm package.

ModuleTop errors raisedCount*
titan-pmconflict (19), timeout (9), notFound (9), internal (5), badRequest (5), unavailable (3)50
titan-databasebadRequest (8), notFound (6), unavailable (4), timeout (1)19
titan-schedulernotFound (7), badRequest (3), timeout (2), conflict (2)14
titan-eventstimeout (3), badRequest (3), notFound (2), internal (2), tooManyRequests (1)11
titan-redisnotFound (5), tooManyRequests (2), timeout (2), conflict (1), badRequest (1)11
titan-healthinternal (6), notFound (1), conflict (1), badRequest (1)9
titan-discoveryconflict (1), badRequest (1)2
titan-notificationsnotFound (1)1
titan-auth— (uses bespoke subclasses below)0
titan-cache— (uses bespoke subclasses below)0
titan-lock— (uses FailureTracker windows + log)0
titan-metrics— (records and continues, doesn't throw)0
titan-ratelimit— (bespoke RateLimitExceededError)0
titan-telemetry-relay— (relays observability; doesn't throw user-visible)0

*Counts are static throw Errors.X(...) callsites in source; per- request error rate depends entirely on input.

Bespoke error classes per module

Some modules export dedicated Error subclasses for cases where extra fields aid programmatic handling.

titan-auth

Official@omnitron-dev/titan-auth

Maintained by the Omnitron team. Independent npm package.

ClassParentWhen thrown
InvalidTokenErrorErrorJWT verification fails (bad signature, malformed)
TokenExpiredErrorInvalidTokenErrorToken's exp is in the past
UnauthorizedErrorErrorMiddleware ran but no credentials presented
SessionRevokedAuthErrorErrorShared-session preset detected revocation
InvalidTokenClaimErrorErrorToken decoded but claims don't match the expected shape
import { InvalidTokenError, TokenExpiredError, UnauthorizedError }
from '@omnitron-dev/titan-auth';

try {
await jwt.verify(rawToken);
} catch (e) {
if (e instanceof TokenExpiredError) return refresh();
if (e instanceof InvalidTokenError) return reject(401);
throw e;
}

titan-database

Official@omnitron-dev/titan-database

Maintained by the Omnitron team. Independent npm package.

ClassWhen thrown
MigrationLockErrorMigrations can't acquire the advisory lock (another pod running)
MigrationChecksumErrorA migration file's checksum doesn't match what was applied

Both signal "don't proceed with the deploy" — the operator must investigate before retry.

titan-pm

Official@omnitron-dev/titan-pm

Maintained by the Omnitron team. Independent npm package.

ClassWhen thrown
PoolBackpressureErrorPool queue is full; caller must back off
import { PoolBackpressureError } from '@omnitron-dev/titan-pm';

try {
await pool.exec('compress', payload);
} catch (e) {
if (e instanceof PoolBackpressureError) {
await sleep(100);
return retry();
}
throw e;
}

titan-ratelimit

Official@omnitron-dev/titan-ratelimit

Maintained by the Omnitron team. Independent npm package.

ClassWhen thrown
RateLimitExceededErrorenforce(key) rejected — caller exceeded limit

Carries:

FieldTypeMeaning
retryAfternumber(ms)Suggested back-off before next attempt
remainingnumberRemaining capacity at error time
resetAtnumberEpoch ms when the window resets

Surface this to clients as a 429 with a Retry-After header.

Common patterns per error kind

notFound (404 / NOT_FOUND)

The most-thrown family. Typical handlers:

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

try {
return await users.findById(id);
} catch (e) {
if (e instanceof TitanError && e.code === ErrorCode.NOT_FOUND) {
return null; // null-on-miss
}
throw e;
}

Modules that throw it: titan-pm (process not found), titan- scheduler (job not found), titan-redis (key namespace not configured), titan-database (connection name missing), titan- health (indicator name missing), titan-discovery (node id), titan-events (history entry), titan-notifications (template / channel).

conflict (409 / CONFLICT)

State-change refused because something already exists or is in an incompatible state. Not retryable without resolving the conflict.

Typical sources: titan-pm (process already running), titan- discovery (lifecycle violation — Cannot start a stopped DiscoveryService), titan-health (indicator name collision), titan-scheduler (job already registered), titan-redis (named client already exists).

timeout (408 / REQUEST_TIMEOUT)

Operation exceeded its configured timeout. Sometimes retryable (if the work is idempotent) — usually surface to caller.

Sources: titan-pm (exec() timeout, health-check timeout), titan-events (handler timeout), titan-scheduler (job timeout), titan-redis (script timeout).

unavailable (503 / SERVICE_UNAVAILABLE)

Transient dependency failure. Retry with exponential backoff.

Sources: titan-database (pool exhausted / connection lost), titan-pm (worker pool empty for class).

tooManyRequests (429 / TOO_MANY_REQUESTS)

Rate / quota exceeded. Always honour retryAfter if provided.

Sources: titan-events (subscription quota), titan-redis (connection backoff).

internal (500 / INTERNAL_ERROR)

Unexpected — usually a bug or environmental anomaly. Log loudly, surface as 500.

Sources: titan-health (indicator output parsing — e.g., df / wmic output), titan-pm (supervisor internal state), titan-events (handler dispatch internal failure).

badRequest (400 / BAD_REQUEST)

Caller-side mistake. Never retry; fix the caller.

Sources: every module — most commonly titan-database (invalid dialect, bad schema name), titan-pm (bad spawn options), titan- scheduler (invalid cron expression).

Handling errors generically

The TitanError class is the unifying shape:

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

try {
return await someService.doWork(input);
} catch (e) {
if (!(e instanceof TitanError)) throw e; // not a framework error

switch (e.code) {
case ErrorCode.NOT_FOUND: return null;
case ErrorCode.UNAUTHORIZED: return reject(401);
case ErrorCode.FORBIDDEN: return reject(403);
case ErrorCode.TOO_MANY_REQUESTS: return scheduleRetry(e);
case ErrorCode.REQUEST_TIMEOUT: return idempotent ? retry() : reject(408);
case ErrorCode.SERVICE_UNAVAILABLE: return retryWithBackoff();
default: throw e;
}
}

Per-call error metadata travels in e.details:

catch (e) {
if (e instanceof TitanError && e.code === ErrorCode.NOT_FOUND) {
logger.warn({ resource: e.details?.resource }, 'lookup miss');
}
}

Throwing your own

In your own services, prefer the Errors namespace over bespoke subclasses unless you need extra fields:

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

if (!user) throw Errors.notFound('user', id);
if (user.locked) throw Errors.conflict('user account is locked');
if (input.amount <= 0) throw Errors.badRequest('amount must be > 0');

Cross-the-wire serialisation, class identity, and code-based routing all work out of the box.

Retryability matrix

A field guide for your retry middleware:

CodeRetryable?Backoff
BAD_REQUEST
UNAUTHORIZED
FORBIDDEN
NOT_FOUND
CONFLICTresolve state first
VALIDATION_ERROR
TOO_MANY_REQUESTShonour retryAfter
REQUEST_TIMEOUTonly if idempotent
SERVICE_UNAVAILABLEexponential
INTERNAL_ERRORwith budget
NOT_IMPLEMENTED

titan-resilience ships pre-built retry and circuit-breaker primitives that already encode this matrix — wire one of those rather than hand-rolling.

Anti-patterns

  • Catching Error and swallowing. Loses the typed error identity. Always narrow to TitanError and inspect code.
  • Mapping every error to 500. The whole point of typed codes is that the client gets actionable status. Forward code.
  • Throwing Errors.internal for user mistakes. A bad input is badRequest or validation, not internal. internal should mean the developer is surprised.
  • Logging the full token in InvalidTokenError. Log kid / code only; the token may be re-usable elsewhere.
  • Treating MigrationLockError as transient. It usually means another deploy is mid-flight; resolve manually, don't retry.

See also