Skip to main content

Timeouts

Handle cases where expected events never arrive.

Setting Timeouts​

Use ctx.setTimeout() to set a timeout:

.on('OrderSubmitted')
.handle(async (msg, state, ctx) => {
// Set 30 minute timeout for payment
ctx.setTimeout(30 * 60 * 1000);

await ctx.publish({ type: 'RequestPayment', orderId: state.orderId });
return { ...state, status: 'awaiting_payment' };
})

Handling Timeout Expiry​

When a timeout expires, SagaTimeoutExpired is published:

.on('SagaTimeoutExpired')
.when(state => state.status === 'awaiting_payment')
.handle(async (msg, state, ctx) => {
// Payment didn't arrive in time - cancel order
await ctx.publish({
type: 'OrderCancelled',
orderId: state.orderId,
reason: 'Payment timeout',
});

return { ...state, status: 'cancelled' };
})

Clearing Timeouts​

Clear a timeout when the expected event arrives:

.on('PaymentCaptured')
.handle(async (msg, state, ctx) => {
ctx.clearTimeout(); // Cancel the timeout
return { ...state, status: 'paid' };
})

Checking Timeout Status​

.on('SomeMessage')
.handle(async (msg, state, ctx) => {
const remaining = ctx.getTimeoutRemaining();
if (remaining !== null) {
console.log(`Timeout expires in ${remaining}ms`);
}
return state;
})

Timeout Metadata​

Timeout info is stored in saga metadata:

interface SagaStateMetadata {
timeoutMs?: number | null; // Timeout duration
timeoutExpiresAt?: Date | null; // When it expires
}

Best Practices​

Set Timeouts Early​

Set timeouts when entering a waiting state:

.on('OrderSubmitted')
.handle(async (msg, state, ctx) => {
ctx.setTimeout(30 * 60 * 1000); // ✅ Set timeout immediately
// ...
})

Clear on Success​

Always clear timeouts when the expected event arrives:

.on('PaymentCaptured')
.handle(async (msg, state, ctx) => {
ctx.clearTimeout(); // ✅ Clear timeout
// ...
})

Use State Guards​

Combine with state guards for safety:

.on('SagaTimeoutExpired')
.when(state => state.status === 'awaiting_payment') // ✅ Only handle if still waiting
.handle(...)