hooksentinel
Errors

handler_error

hooksentinel error handler_error — your onEvent callback threw an uncaught error. hooksentinel responds 500 so the provider retries delivery.

Summary

FieldValue
Codehandler_error
HTTP status500
RetryableYes

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

On this page