Skip to content

Error Handling & Retries

By default, failed steps (flow.do, flow.request, flow.start, flow.dialog initiation) are retried a few times with a constant delay. You can customize this behavior globally for a workflow or per-step.

Retry Configuration (RetryStrategy):

interface RetryStrategy {
limit?: number | false; // Max attempts (false = infinite, 0 = no retries). Default: 5
delay?: Duration; // Delay between retries (ms or string like '1 minute'). Default: '1 minute'
backoff?: 'constant' | 'linear' | 'exponential'; // Backoff strategy. Default: 'constant'
}

Applying Retries:

  1. Workflow Defaults: Set in the defineWorkflow configuration under the defaults key. See Defining Workflows.

    defineWorkflow(
    {
    // ... other config
    defaults: { retries: { limit: 3, delay: '30 seconds', backoff: 'exponential' } },
    },
    async (flow) => {
    /* ... */
    },
    );
  2. Per-Step Options: Override defaults by passing retries in the options argument of an activity.

    await flow.do(
    'call-flaky-api',
    { retries: { limit: 10, delay: '5 seconds' } },
    async ({ use }) => {
    /* ... */
    },
    );
    // Disable retries for a specific step
    await flow.do(
    'critical-once-only-task',
    { retries: false }, // Equivalent to { retries: { limit: 0 } }
    async ({ use }) => {
    /* ... */
    },
    );

Choose retry strategies appropriate for the type of failure expected (e.g., exponential backoff for overloaded services).


IdentityFlow provides structured error handling through custom error classes and utility functions exported from @identity-flow/sdk. This allows for more precise error management than relying solely on standard JavaScript Error objects.

Key Exports from @identity-flow/sdk (related to errors):

  • Error Classes:
    • WorkflowError: Base class for workflow-related errors.
    • NonRetryableError: For errors that should not trigger the retry mechanism.
    • ValidationError: For issues related to data validation (often thrown automatically by schema checks, but can be used manually).
    • TimeoutError: A specific NonRetryableError for timeouts.
    • AssertionError: Thrown by flow.assert() or can be used for custom assertions.
    • LockedError: Indicates a resource is locked.
  • Type Guard Functions:
    • isNonRetryableError(error: unknown): error is NonRetryableError
    • isValidationError(error: unknown): error is ValidationError
    • isLockedError(error: unknown): error is LockedError
    • isAbortError(error: unknown): error is AbortError (for handling AbortSignal related errors)
  • Other Utilities:
    • throwIfAborted(value: unknown): Checks and throws if an AbortSignal has been aborted.

Throwing Specific Errors:

When an activity function needs to signal a failure, throwing an instance of these specific error classes provides more context to the engine and to any catching logic.

import { NonRetryableError, ValidationError, defineWorkflow } from '@identity-flow/sdk';
// ... in an activity function ...
if (criticalConditionFailed) {
throw new NonRetryableError('Critical condition failed, will not retry.', {
code: 'CRITICAL_FAILURE',
});
}
if (userInputInvalid) {
throw new ValidationError('Invalid user input provided.', {
issues: [{ message: 'Email is required', path: ['email'] }],
});
}

Catching and Checking Errors:

You can use standard try...catch blocks and the provided type guards to handle errors gracefully and specifically.

import { defineWorkflow, isNonRetryableError, isValidationError } from '@identity-flow/sdk';
// ... in execute function or an activity ...
try {
await flow.do('risky-operation', async () => { /* ... may throw custom errors ... */ });
} catch (error) {
if (isValidationError(error)) {
flow.warn('Validation failed during risky operation:', error.message, error.issues);
// Handle validation error, maybe inform user or return specific result
return { status: 'VALIDATION_ERROR', issues: error.issues };
} else if (isNonRetryableError(error)) {
flow.error('Non-retryable error in risky operation:', error.message, { code: error.code });
// Perform cleanup for non-retryable failure
await flow.do('cleanup-non-retryable', async () => { /* ... */ });
return { status: 'FATAL_ERROR', reason: error.message };
} else {
// Generic error, might be retried by the engine or bubble up
flow.error('Risky operation failed:', error);
throw error; // Re-throw if you want the engine to handle retries or fail the workflow
}
}
  • Unrecoverable Errors: If an error occurs outside of a configured retry mechanism (e.g., a NonRetryableError is thrown, or after all retries for a retryable error are exhausted), the workflow instance will typically transition to the FAILED state.

Using these specific error types and utilities enhances the robustness and debuggability of your workflows.

The flow.assert(condition, messageOrError) utility is a convenient way to perform simple precondition checks within your workflow logic. If the condition is false, it throws an AssertionError (which is a NonRetryableError) with the given message, or throws the provided error object.

Signature:

flow.assert(condition: any, messageOrError?: string | ErrorDetails | Error): asserts condition;

Example:

async (flow) => {
const { orderId, items } = flow.params;
flow.assert(orderId, 'Order ID is required.');
flow.assert(items && items.length > 0, {
message: 'Order must contain items.',
code: 'EMPTY_ORDER',
});
// ... rest of the workflow logic ...
};

If an assertion fails, the workflow will typically terminate unless the AssertionError is caught and handled specifically.


Complete your understanding of workflow development with these topics: