NestflowJS LogoNestflowJS

Retry and Error Handling

Handle retries with exponential backoff and jitter. Built-in support for UnretriableException and payload validation.

@WithRetry Decorator

Add @WithRetry to any @OnEvent handler to enable automatic retries with configurable backoff:

import { OnEvent, Entity, Payload, WithRetry } from 'nestflow-js/core';
import { RetryStrategy } from 'nestflow-js/core';

@OnEvent('payment.authorized')
@WithRetry({
  handler: 'handleAuthorized',
  maxAttempts: 3,
  strategy: RetryStrategy.EXPONENTIAL_JITTER,
  initialDelay: 500,
  backoffMultiplier: 2,
  maxDelay: 10000,
  jitter: true,
})
async handleAuthorized(@Entity() payment: Payment) {
  const result = await this.paymentGateway.authorize(payment);
  return { authorizationCode: result.code };
}

Configuration

PropertyTypeDefaultDescription
handlerstring(required)Name of the handler method
maxAttemptsnumber(required)Maximum number of attempts (including the initial try)
strategyRetryStrategyEXPONENTIAL_JITTERBackoff strategy
initialDelaynumber1000Base delay in milliseconds
backoffMultipliernumber2Multiplier applied on each attempt
maxDelaynumber60000Upper bound for computed delay in ms
jitterboolean | numbertruetrue for full jitter, 0-1 for partial jitter percentage

Backoff Strategies

NestflowJS supports three retry strategies via the RetryStrategy enum:

RetryStrategy.FIXED

Constant delay between attempts. Use when you want predictable timing.

Attempt 0: 1000ms
Attempt 1: 1000ms
Attempt 2: 1000ms

RetryStrategy.EXPONENTIAL

Delay doubles (or multiplies) on each attempt: initialDelay * backoffMultiplier^attempt, capped at maxDelay.

Attempt 0: 1000ms
Attempt 1: 2000ms
Attempt 2: 4000ms
Attempt 3: 8000ms (capped at maxDelay)

RetryStrategy.EXPONENTIAL_JITTER (default)

Exponential backoff with randomized jitter. This is the AWS-recommended approach to prevent thundering herd problems when many clients retry simultaneously.

  • Full jitter (jitter: true): random(0, min(maxDelay, initialDelay * 2^attempt))
  • Partial jitter (jitter: 0.5): adds +/-50% randomization to the exponential delay

UnretriableException

Some errors should never be retried: invalid input, business rule violations, not-found errors. Throw UnretriableException to skip retries immediately:

import { UnretriableException } from 'nestflow-js/exception';

@OnEvent('payment.authorized')
@WithRetry({
  handler: 'handleAuthorized',
  maxAttempts: 3,
  strategy: RetryStrategy.EXPONENTIAL,
  initialDelay: 500,
})
async handleAuthorized(@Entity() payment: Payment, @Payload() payload: any) {
  // Permanent failure — don't retry
  if (payload?.cardNumber === '0000-0000-0000-0000') {
    throw new UnretriableException('Invalid card number');
  }

  // Transient failure — will be retried
  const result = await this.paymentGateway.authorize(payment);
  return { authorizationCode: result.code };
}

When UnretriableException is thrown:

  • The entity moves to the workflow's failed state
  • No further retry attempts are made
  • The orchestrator returns { status: 'final', state: failedState }

Failed State

When a handler throws (and retries are exhausted), the orchestrator automatically:

  1. Catches the error
  2. Updates the entity to the workflow's configured failed state
  3. Re-throws the error (so adapters can handle it)
@Workflow<Payment, PaymentEvent, PaymentState>({
  name: 'PaymentWorkflow',
  states: {
    finals: [PaymentState.COMPLETED, PaymentState.FAILED],
    idles: [PaymentState.INITIATED],
    failed: PaymentState.FAILED,  // <-- entity moves here on unhandled errors
  },
  // ...
})

Payload Validation

Validate incoming event payloads before your handler executes. NestflowJS is schema-library agnostic — bring your own validation.

Setup

Provide a payloadValidator function when registering the module:

import { WorkflowModule } from 'nestflow-js/core';
import { z } from 'zod';

@Module({
  imports: [
    WorkflowModule.register({
      entities: [{ provide: 'entity.payment', useClass: PaymentEntityService }],
      workflows: [PaymentWorkflow],
      // Zod example
      payloadValidator: (schema, payload) => (schema as z.ZodSchema).parse(payload),
    }),
  ],
})
export class PaymentModule {}

Usage with @Payload

Pass your schema to @Payload():

const SubmitPaymentSchema = z.object({
  amount: z.number().positive(),
  currency: z.string().length(3),
  cardNumber: z.string(),
});

@OnEvent('payment.initiated')
async handleInitiated(
  @Entity() payment: Payment,
  @Payload(SubmitPaymentSchema) data: z.infer<typeof SubmitPaymentSchema>,
) {
  // data is validated and typed
  return { amount: data.amount };
}

If validation fails, the framework throws a BadRequestException wrapping the validation error.

Alternative: Joi

// In module registration
payloadValidator: (schema, payload) => {
  const { value, error } = (schema as Joi.Schema).validate(payload);
  if (error) throw error;
  return value;
},

RetryBackoff Utility

For custom retry logic outside of @WithRetry, use the RetryBackoff utility directly:

import { RetryBackoff, RetryStrategy } from 'nestflow-js/core';

const delay = RetryBackoff.calculateDelay(attempt, {
  handler: 'myHandler',
  maxAttempts: 5,
  strategy: RetryStrategy.EXPONENTIAL_JITTER,
  initialDelay: 1000,
  backoffMultiplier: 2,
  maxDelay: 30000,
  jitter: true,
});

// Alternative: decorrelated jitter
const nextDelay = RetryBackoff.decorrelatedJitter(previousDelay, 1000, 30000);