Diagnosing Failures

A message failed in production. How do you find out why?

This page ties together the signals Benzene emits when something goes wrong in a message handler or in the middleware pipeline: what you get with nothing configured, the recommended middleware stack that makes failures easy to trace, and how the same failure surfaces across logs, traces, and metrics. For setting up observability backends see Monitoring & Diagnostics; for catching exceptions and mapping them to responses see Global Error Handling.

Two kinds of failure

Benzene distinguishes two failure modes, and they surface differently:

The distinction matters when reading logs: an unsuccessful result and a rejection are different events with different log lines, and a stack trace only exists for the second kind. See Message Results for the status vocabulary.

What you get with nothing wired

Even with no logging or diagnostics middleware added, the router emits a baseline error signal so failures aren't completely silent:

Situation Signal Source
Topic missing from the message warning "Topic is missing" MessageRouter
No handler registered for the topic warning "No handler found for topic " MessageRouter
Handler/middleware throws Transport-dependent — see the catch matrix the transport application

These answer "did my message route, and did it fail?" from a plain log tail — but they carry no correlation id, no trace id, and no per-message processing time. The recommended stack below adds all of that.

These log calls only reach a sink if a logging provider is configured. addBenzene(...) makes ILogger resolvable, but with no provider every log call is a no-op — nothing appears regardless of what middleware you add. See Monitoring — Logging.

Add these to each transport pipeline, in this order, before useMessageHandlers(...). The free functions (useW3CTraceContext, useBenzeneEnrichment) take the builder first; useExceptionHandler and useLogResult are pipeline-builder members (see Common Middleware):

import { addBenzene, useMessageHandlers } from '@benzene/core-message-handlers';
import { InlineAwsLambdaStartUp } from '@benzene/aws-lambda-core';
import { useSqs } from '@benzene/aws-lambda-sqs';
import { addDiagnostics, useW3CTraceContext, useBenzeneEnrichment } from '@benzene/diagnostics';
import { SqsMessageContext } from '@benzene/aws-lambda-sqs';

const entryPoint = new InlineAwsLambdaStartUp()
  .configureServices((services) => {
    addBenzene(services);
    addDiagnostics(services); // span per middleware — marks failing spans Error, tagged benzene.*
  })
  .configure((app) =>
    useSqs(app, (sqs) => {
      useW3CTraceContext(sqs); // 1. FIRST — continue the caller's distributed trace
      useBenzeneEnrichment(sqs); // 2. attach invocationId/traceId/topic/transport/handler to every log
      sqs
        .useExceptionHandler((context: SqsMessageContext) => {
          context.isSuccessful = false; // 3. catch + log thrown exceptions; settle the message
        })
        .useLogResult(() => {}); // 4. one structured "BenzeneResult" info line per successful message
      useMessageHandlers(sqs, ProcessOrderHandler);
    }),
  )
  .build();

What each layer contributes when something fails:

Layer On failure it gives you
addDiagnostics(services) The failing span is marked SpanStatusCode.ERROR with an exception event, tagged benzene.transport/benzene.topic/benzene.handler/benzene.status. A no-op until an OpenTelemetry SDK is registered — cheap otherwise (the span is non-recording).
useW3CTraceContext(app) The trace continues from the caller instead of starting fresh, so the failure is on the same trace as whatever triggered it. Must be first; safe when the header is absent.
useBenzeneEnrichment(app) invocationId, traceId, spanId, topic, transport, handler on every log line in the pipeline (each omitted gracefully when unavailable). Portable across all transports.
app.useExceptionHandler(...) An error log plus your callback decides how the transport settles. The callback is transport-specific — see Global Error Handling.
app.useLogResult(...) One info "BenzeneResult" line with processTime per successful message (it does not log on a throw; a handler-body throw is logged by MessageHandler as error).

Add useBenzeneMetrics(app) too if you want the failure to also move a counter/histogram — see below.

How a failure shows up across the three signals

In logs

An unsuccessful result (no throw) produces the router warning; a thrown error produces an error with a stack. Both carry the enrichment fields, so traceId ties every line of one message together (and across services), topic + status narrows to a failure kind, and transport separates which adapter handled it.

In traces

With addDiagnostics() and an OpenTelemetry SDK registered (Monitoring — reaching a real backend), the failing middleware's span carries SpanStatusCode.ERROR and an exception event with the stack, so a trace viewer (Jaeger, Tempo, Application Insights) points straight at the stage that threw. Every span is tagged with topic/handler, and useW3CTraceContext() keeps it on the caller's trace.

Unlike .NET's ActivitySource.StartActivity (which returns null when no listener is attached), the OpenTelemetry JS API always returns a span object. Benzene's wrapper checks span.isRecording() instead, so with no SDK registered the span ends immediately and the inner middleware is called directly — near-zero cost. See Monitoring.

In metrics

With useBenzeneMetrics(app), every message moves two instruments tagged topic/transport/result:

Ordering footguns

A few middleware silently do nothing if wired in the wrong order — no error, just missing data:

What reaches your logs per transport

When a handler throws, what Benzene logs (before any middleware you add) depends on the transport application that owns the invocation:

Transport Exception handling Benzene-logged with context?
AWS Lambda SQS catch per record → batch-item-failure Yeserror per failed record
AWS Lambda SNS / DynamoDb / Kinesis catch per record Yeserror per record
AWS Lambda S3 / EventBridge / Kafka propagates to the Lambda host Only the runtime's raw error (no Benzene context) — wire useExceptionHandler/useLogResult to get a context log line
Azure Functions triggers escalation to the Functions host Host logs; Benzene adds structured logs where the app catches
HTTP (API Gateway / Express) maps to a status code Exception → the app's error path / useExceptionHandler if wired

Two takeaways: the AWS batch family already attributes a throw to a specific record; the single-event AWS Lambda sources (S3/EventBridge/Kafka) lean on the platform host, so wiring useExceptionHandler/useLogResult there is what gets you a Benzene-context log line instead of a bare stack trace in CloudWatch.

Checklist

  1. Was it an unsuccessful result or a throw? Look for the router warning (result) vs. an error with a stack (throw).
  2. Grep by traceId (or invocationId) to pull every line of that one message together.
  3. Check the topic/handler tags — a "No handler found for topic" warning means a routing/registration problem, not a handler bug.
  4. Open the trace if you export spans — the ERROR-status span names the failing stage.
  5. Nothing in the logs at all? Confirm a logging provider is configured (not just resolvable loggers), and that useLogResult/useExceptionHandler sit before useMessageHandlers(...).

See Also