Never Let an Exception Cross the Bus

Table of Contents

+#+filetags: :pattern:nats:error-handling: +#+created: 2026-10-14 +#+updated: 2026-10-14 +#+pattern_group: error-handling

This page defines the nats pattern Never Let an Exception Cross the Bus in the NATS service patterns language. It is bus-specific: it follows from how a message callback is delivered, not from any pattern in the literature.

1. Also known as

  • The callback boundary; the C-frame rule.

2. Context

Every subscription is delivered by one callback that the NATS client library invokes on its own thread. In C++, that callback is invoked across a C frame, and the library knows nothing of exceptions.

3. Problem

What happens to an exception that leaves a message handler?

4. Forces

  • An exception crossing a C frame is undefined behaviour and in practice calls `std::terminate`, killing the process.
  • The process death leaves no line in the service's log, so the cause is invisible.
  • A handler does real work — decoding, a database call — and all of that can throw.
  • A message that kills its consumer also takes every other subscription with it.

5. Solution

The handler reports and returns; nothing escapes. The transport provides the outermost catch, and each handler provides its own inner one:

// The transport's callback: the last line of defence, on a library thread.
void on_msg(natsConnection*, natsSubscription*, natsMsg* msg, void* ud) {
    // read the subject first, cheaply, so the catch below can name it
    try {
        cl->handler(extract_message(msg));
    } catch (const std::exception& e) {
        BOOST_LOG_SEV(lg(), error)
            << "Unhandled exception in handler for '" << subject << "': " << e.what();
    } catch (...) {
        BOOST_LOG_SEV(lg(), error) << "Unknown exception in handler for '" << subject << "'";
    }
}

Three rules:

  • A handler reports a failure, it does not throw. An exception from the service layer becomes a failed answer in the body; see Report a Failure in the Body.
  • A caught exception is never simply swallowed. The handler replies with the failure, or — where there is no reply subject — logs it as the whole account of what happened.
  • A durable handler must not acknowledge by escaping. Under an auto-ack subscription, returning normally acknowledges the message; see Acknowledge What Was Handled.

6. Failure handling

Concern What the pattern does
Errors The exception is caught at the boundary and recorded, and the process survives.
Timeouts A caller whose handler threw and did not reply waits out its deadline; the reply is the handler's responsibility.
Retries A core-NATS message is lost; a durable one is redelivered only if it was not acknowledged.
Idempotency Retrying after a throw requires the handler to be safe to run again.
Observability The boundary logs the subject and the exception text, so a crash is replaced by a line.

7. Pitfalls

  • An exception escaping a handler. The service dies with no log line for the cause, taking every other subscription with it.
  • Catching and returning silently. The caller waits out its deadline and the log says nothing.
  • A catch that acknowledges. On a durable subscription, a caught failure that returns normally is acknowledged as done.
  • Reading the message after destroying it. State read in the catch must be captured before the message is freed.

8. Resulting context

Benefits
One bad message costs one message, not the whole service.
Liabilities
The handler must be written so every path either replies or deliberately does not.

9. In ORE Studio

The callback in `ores.nats` destroys the cnats message before invoking the handler and catches every exception, logging the subject and the text. Every generated handler wraps the service call in its own try/catch and replies with a failed body. Hand-written handlers follow the same shape.

10. Related patterns

11. Sources

12. See also

  • NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.

Emacs 29.3 (Org mode 9.6.15)