Report a Failure in the Body

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 Report a Failure in the Body in the NATS service patterns language.

1. Also known as

  • The canonical response envelope.

2. Context

A handler reached the service layer and the service reached a decision: the row is missing, the version conflicts, a business rule denies the write, or the store threw. Every one of those is a legitimate answer to a well-formed request.

3. Problem

How does a service report what it decided, when "denied" and "failed" and "succeeded" are all normal outcomes of a request it handled?

4. Forces

  • A refusal is about the caller's right to ask; a decision is about the request itself, and the caller must be able to tell them apart.
  • A caller needs enough structure to branch, not a prose message to parse.
  • An exception from the store is a real fault and must not read as a business rule declining.
  • The same envelope must serve every entity, so a new entity invents nothing.

5. Solution

Answer with the response type, whose `result` carries the outcome:

auto response = svc.update(*req);      // fills result.outcome / code / message
reply(nats_, msg, response);

// An exception from the store is not a decision: report it as a failure.
catch (const std::exception& e) {
    UpdateResponse failure;
    failure.result.outcome = ores::utility::domain::outcome::failed;
    failure.result.code = "internal_error";
    failure.result.message = e.what();
    reply(nats_, msg, failure);
}

The outcome is a closed set, so a caller can be exhaustive:

Outcome Means
ok The request succeeded
invalid The request is well-formed but its content is not acceptable
denied A business rule refused it
missing The addressed record does not exist
conflict The version precondition failed
unavailable A dependency the operation needs is not available
failed The operation ran and could not complete

Two rules:

  • The handler decides nothing. It proves the request, checks the permission, decodes the payload, calls the service and carries the answer back. The outcome the caller reads is the service's, so the two cannot disagree.
  • A thrown exception becomes failed, never a business outcome. A store error is not "denied"; collapsing the two hides an outage inside normal answers.

6. Failure handling

Concern What the pattern does
Errors Every decided outcome is an answer; the transport reports success.
Timeouts Unaffected: the reply arrives as soon as the service decides.
Retries conflict and unavailable are the outcomes that may be retried; the others will not change.
Idempotency A retry re-runs the service, so the operation's idempotency governs safety.
Observability The handler logs the subject and the outcome, so a decision and its reason are in the log.

7. Pitfalls

  • A failed outcome for a refusal. An authorization failure is reported as failed, and the caller diagnoses the payload.
  • A prose message with no code. The caller cannot branch, so it either ignores the failure or parses text.
  • An empty message on failure. The log has the outcome and nothing about which field or rule produced it.
  • A new outcome value at one call site. The set is closed so callers can be exhaustive; a fresh string breaks that.

8. Resulting context

Benefits
One reply shape for every entity, and a caller that can branch on a closed set.
Liabilities
"Denied" and "failed" can be confused by a careless handler.

9. In ORE Studio

`ores::utility::domain::result` carries `outcome`, `code`, `message` and `fields`; every generated handler replies with the response type and maps an exception to internal_error with outcome failed. See NATS entity protocol specification.

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)