handler_error
hooksentinel error handler_error — your onEvent callback threw an uncaught error. hooksentinel responds 500 so the provider retries delivery.
Summary
| Field | Value |
|---|---|
| Code | handler_error |
| HTTP status | 500 |
| Retryable | Yes |
What caused it
Your onEvent callback threw (or rejected, for an async handler) instead of completing normally. hooksentinel catches this, marks the event as not processed in the idempotency store (calling release() so a redelivery gets a fair retry rather than being silently deduplicated against a failed attempt), and responds 500 so the provider's own retry mechanism redelivers the event later.
This is by design: business logic failures (a downstream API being down, a database write failing, an unexpected null in the payload your code didn't guard against) are exactly the case at-least-once delivery and retries exist to paper over, as long as your handler is safe to run again — see Idempotency for making sure it is.
The fix
This isn't a hooksentinel bug to fix — it's your application code surfacing a real error. Start by finding out what threw:
onError: async (error, ctx) => {
if (error.code === 'handler_error') {
logger.error('webhook handler threw', {
provider: ctx.provider,
eventId: ctx.eventId,
cause: error.cause, // the original error your onEvent threw
});
}
},error.cause holds the original error your onEvent threw, so you get the real stack trace rather than a generic wrapper message.
Make sure your handler is actually safe to retry before relying on the provider's redelivery to paper over transient failures — a handler that partially applies a mutation before throwing (e.g. charges a customer, then throws before recording the charge) will double-charge on retry rather than safely retrying. Use the same-transaction recipe so a partial failure can't commit a side effect without also recording that it happened.
If a specific failure is expected and shouldn't trigger a provider retry (e.g. a business rule rejection that will never succeed no matter how many times it's retried), catch it inside onEvent and don't rethrow — a webhook handler that always resolves successfully for events it intentionally chooses not to act on won't produce handler_error at all:
onEvent: async (event) => {
if (event.type !== 'checkout.session.completed') return;
try {
await fulfillOrder(event.data.object.id);
} catch (err) {
if (err instanceof OrderAlreadyCancelledError) {
logger.warn('order was cancelled before fulfillment ran', { sessionId: event.data.object.id });
return; // don't rethrow — retrying this will never succeed
}
throw err; // genuinely transient — let it retry
}
},Last updated on
idempotency_store_error
hooksentinel error idempotency_store_error — the configured idempotency store (Redis, Prisma, etc.) was unreachable or errored while checking or marking an event.
provider_verification_error
hooksentinel error provider_verification_error — a provider-specific check beyond the generic signature/timestamp validation failed.