@onlineapps/conn-infra-error-handler
v4.0.0
Published
Unified error handling with retry strategies, circuit breaker, and compensation patterns
Maintainers
Readme
Status: current Owns: the error-handling surface a business service is given —
error-handler-corebound to that service's monitoring
Uniform: library/connector
Duty sections that apply:
all: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-README, L-README-REGION, L-CONSUMERconnector: L-CONNECTOR-ENV
@onlineapps/conn-infra-error-handler
Overview
Error handling connector for business services. Wraps @onlineapps/error-handler-core and integrates with @onlineapps/conn-base-monitoring for unified error handling and logging.
Note: This connector is for business services using ServiceWrapper. Infrastructure services should use @onlineapps/error-handler-core directly.
Installation
npm install @onlineapps/conn-infra-error-handlerFeatures
- Error Classification - Automatic error type detection (TRANSIENT, BUSINESS, FATAL, etc.)
- Retry Logic - Exponential backoff for transient errors
- Circuit Breaker - Protection against cascading failures
- Compensation - Rollback operations for failed workflows
- Unified Logging - Structured error logging via monitoring-core
Architecture
This connector wraps @onlineapps/error-handler-core and integrates with @onlineapps/conn-base-monitoring:
conn-infra-error-handler (wrapper)
└─> error-handler-core (core logic)
└─> monitoring-core (logging)See Error Handling Standard for the cross-cutting standard and package landscape.
Usage
Via ServiceWrapper — how a biz service gets one
A biz service does not construct this connector: ServiceWrapper builds one
during initialize() and exposes it as wrapper.errorHandler. What the service
declares is the errorHandling block inside the wrapper section of its
config.json — not an errorHandling argument to the wrapper's constructor,
which takes config, operations, serviceRoot and serviceBaseDir.
The wrapper reads five keys from that block and hands them on as the handling
object described below: maxRetries, retryDelay, retryMultiplier,
circuitBreakerEnabled, compensationEnabled. dlqEnabled was the sixth until
the wrapper retired it — dead-lettering left this package with routeToDLQ()
(see the CHANGELOG entry for it), so the key lost its only reader, and a config
still writing it is now REFUSED by name rather than dropped in silence. Nothing
of the wrapper's own travels on that object either: handling.mqClient, which it
used to attach "for DLQ routing", went with the same reader. They are the
wrapper's keys, not this package's — their meaning and their defaults live with
service-wrapper, and a key the
wrapper does not read changes nothing here.
Direct usage — import and construction
The module's default export is the connector class; there is no named
ErrorHandlerConnector property on it. Alongside the class the module exports
ErrorTypes, ErrorCodes, CircuitOpenError, create(config) and VERSION.
const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
const { ErrorTypes, CircuitOpenError, VERSION } = require('@onlineapps/conn-infra-error-handler');
const { init: initMonitoring } = require('@onlineapps/conn-base-monitoring');
const monitoring = await initMonitoring({ serviceName: 'my-service', mode: 'light' });
const errorHandler = new ErrorHandlerConnector({
serviceName: 'my-service', // required
monitoring, // required: conn-base-monitoring instance
logger, // required: info/warn/error/debug
serviceVersion: '1.0.0', // optional
environment: 'production', // optional
handling: { // optional, passed straight to error-handler-core
maxRetries: 3,
retryDelay: 1000,
retryMultiplier: 2,
circuitBreakerEnabled: true,
}
});Every required input is checked in the constructor, before the core is built — a missing one throws there, naming the key and the fix, never at first use:
serviceName— refused by the connector itself.monitoring— refused by the connector itself.logger— refused byassertLogger()of@onlineapps/logger-contract, which owns the contract; the message namesErrorHandlerConnectoras the class the caller constructed.
Public methods
One line each; the parameters, return shapes and @throws are the generated
detail in API.md (npm run docs renders it from the JSDoc in
src/index.js — never edit it by hand).
classifyError(error)— the error's type fromErrorTypes.shouldRetry(error, attempts)— whether that error at that attempt count is worth another try.calculateBackoff(attempts)— the exponential backoff delay, in milliseconds, for an attempt.executeWithRetry(fn, options)— runs an async function, retrying it per the retry handler.executeWithCircuitBreaker(name, fn, options)— runs an async function behind a named circuit.logError(errorData)— writes one unified error log entry.handleError(errorData)— classify, log, and decide the action (retry / dlq / throw / compensate). The decision is returned to the caller; this package acts on none of it but compensation.registerCompensation(operation, handler)— registers the rollback of an operation.executeCompensation(operation, context)— runs the registered rollback, or returnsnullwhen none is registered.createErrorResponse(error, context)— formats the standard error response for the perimeter.getStats()— the live counters (errors, retries, compensations, circuit breaks, per type) plus the circuit states.resetStats()— clears those counters.getCircuitBreakerState(name)— the state of one circuit.getAllCircuitBreakerStates()— the state of every circuit.
Circuit breaker refusal
An open circuit refuses the call itself — the action is not invoked. The refusal is a type with a code, never a sentence: CircuitOpenError from @onlineapps/error-handler-core, re-exported here so a consumer of this connector alone can recognise it. Every such refusal is counted in getStats().circuitBreaks.
const { CircuitOpenError } = require('@onlineapps/conn-infra-error-handler');
try {
await errorHandler.executeWithCircuitBreaker('user-api', () => userAPI.getUser(id));
} catch (error) {
if (error.code === 'CIRCUIT_OPEN') {
// error.details.name === 'user-api'; error instanceof CircuitOpenError
}
throw error;
}The optional third argument takes only the five options CircuitBreakerManager declares — timeout, errorThresholdPercentage, resetTimeout, rollingCountTimeout, rollingCountBuckets — each checked against its range before the circuit is created. An undeclared key is refused with the accepted ones listed.
Configuration
This package has no configuration of its own — no config schema, no defaults
file, no environment variables it reads. What it accepts is the config object
handed to the constructor above, and config.handling is passed straight through
to @onlineapps/error-handler-core, which owns those keys and their defaults.
The keys are listed in the constructor's JSDoc, rendered into API.md.
Error Types
Classification is @onlineapps/error-handler-core's and this connector only delegates to it. It reads, in order: an explicit error.type, then error.code, then the HTTP status in error.status — the field the error contract names (api/docs/biz/70-contracts/error-handling.md §1) — and only then the message patterns. error.statusCode is not read: an error branded with it alone falls through to the patterns and ends as UNKNOWN, which is not retried. Brand errors with code + status.
Transient Errors
Automatically retried:
- Network errors (ECONNREFUSED, ETIMEDOUT)
- Service unavailable (503)
- Rate limiting (429)
- An open circuit (
CIRCUIT_OPEN) — the breaker heals itself after its reset timeout - Temporary database issues
Permanent Errors
Classified dlq on the first failure — handleError() returns the action, it
routes nothing. Dead-lettering is the consumer policy of
@onlineapps/mq-client-core, the only rail for biz and infra alike
(mq-consumer-contract
002 bod 3):
- Validation errors (400)
- Authentication errors (401)
- Not found errors (404)
- Business logic errors
Error context and the log entry
logError() and handleError() take { moduleName, operation, error, context }
and delegate to @onlineapps/error-handler-core, which builds the unified log
entry — the error, the context, the handling decision and the metadata. The
entry's shape is the core's (UnifiedLogSchema) and is not repeated here.
context is passed through whole, so whatever a caller puts in it reaches the
entry unchanged.
Testing
npm test # Run all tests
npm run test:unit # Unit tier (tests/unit/**)Dependencies
Declared in package.json, pinned exactly; this file does not
repeat them. The monitoring instance the constructor requires is injected by the
caller, so @onlineapps/conn-base-monitoring is not a dependency of this
package.
Related Documentation
- Error Handling Standard — the cross-cutting standard: fail-loudly rules, classification table, core vs connector
- Error handling contract (biz) — what a biz error carries and what a consumer branches on
- Service Wrapper — the consumer that builds this connector
License: MIT — the published version is require('@onlineapps/conn-infra-error-handler').VERSION, read from package.json.
