Error Classification
Different kinds of failure deserve different responses. A 404 is not a 500. A timeout is not an auth failure. Titan provides classifier helpers in two places.
ErrorCode-driven classification
The fastest classification uses the error code itself. For a known
ErrorCode:
import {
ErrorCode,
ErrorCategory,
getErrorCategory,
isClientError,
isServerError,
isRetryableError,
getErrorName,
getDefaultMessage,
} from '@omnitron-dev/titan/errors';
const code = ErrorCode.SERVICE_UNAVAILABLE;
getErrorCategory(code); // ErrorCategory.ServerError (or similar)
isClientError(code); // false — 5xx
isServerError(code); // true
isRetryableError(code); // true — 503 is retryable
getErrorName(code); // 'SERVICE_UNAVAILABLE'
getDefaultMessage(code); // 'Service is currently unavailable'
These helpers operate on the code alone — no need for the full error instance. Useful in middleware and telemetry where you only have the code.
ErrorCategory enum
import { ErrorCategory } from '@omnitron-dev/titan/errors';
ErrorCategory.ClientError // 4xx-equivalent
ErrorCategory.ServerError // 5xx-equivalent
// …additional categories as needed
The mapping from ErrorCode to ErrorCategory is the
implementation's responsibility (getErrorCategory(code)); the
enum is the vocabulary.
isOperationalError(error) — for retry decisions
Operational errors are those caused by external systems (network, disk, database, third-party API) — distinct from programming bugs. They are typically retryable.
import { isOperationalError } from '@omnitron-dev/titan/utils';
if (isOperationalError(e)) {
// Retry, log, alert as transient.
} else {
// Programming error — log with full stack and fix the bug.
}
The classifier recognises:
- Postgres
SQLSTATEcodes in the operational set (connection failure, timeout, serialization, etc.). - Network error codes (
ECONNREFUSED,ETIMEDOUT,EHOSTUNREACH,ENETUNREACH, …). - Specific framework errors with retry hints.
You can extend the operational set with
createOperationalErrorRecorder — see the source in
utils/error-classification.ts.
Putting it together
A typical retry / telemetry pattern:
import { isOperationalError } from '@omnitron-dev/titan/utils';
import { TitanError, isRetryableError } from '@omnitron-dev/titan/errors';
async function callWithRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (e) {
lastError = e;
const retryable =
isOperationalError(e) ||
(e instanceof TitanError && isRetryableError(e.code));
if (!retryable) throw e;
await sleep(computeBackoff({ attempt, baseMs: 100, maxMs: 5_000 }));
}
}
throw lastError;
}
For ready-made resilience, use retry() and CircuitBreaker from
@omnitron-dev/titan/utils/resilience.ts — see
Resilience.
Telemetry partitioning
catch (e) {
const code =
e instanceof TitanError ? e.code : ErrorCode.INTERNAL_ERROR;
metrics.counter('rpc.errors', {
code: String(code),
category: getErrorCategory(code),
}).inc();
throw e;
}
Now your dashboards can show "client errors" separately from "server errors" — the former are usually client bugs; the latter usually indicate something wrong on your side.
Alerting policy
app.on(ApplicationEvent.Error, ({ error }) => {
const code = error instanceof TitanError ? error.code : ErrorCode.INTERNAL_ERROR;
if (isServerError(code) || isRetryableError(code)) {
oncall.page({ error });
}
if (code === ErrorCode.UNAUTHORIZED || code === ErrorCode.FORBIDDEN) {
securityFeed.report(error);
}
});
Anti-patterns
- Retrying based on
instanceof Error. Catches everything, including programming bugs (which will fail again) and validation errors (which won't change). Classify before retrying. - String-matching error messages. Brittle. If the message changes (i18n, library upgrade), classification breaks silently. Use codes.
- Treating all 5xx as transient. A 500 is usually a bug, not a
transient failure. Be specific about which codes warrant retry
(
isRetryableError(code)is the safe default).
→ Back to Errors Overview.