Shipeasy
SDKsReferenceJava

Error reporting — `see()`

The Java SDK ships the see() surface: structured error reporting that documents an error's product consequence, not just its stack. It mirrors @shipeasy/sdk…

Generated from the SDK's own /docs/ — also served raw at https://shipeasy-ai.github.io/sdk-java/pages/error-reporting.md.

The Java SDK ships the see() surface: structured error reporting that documents an error's product consequence, not just its stack. It mirrors @shipeasy/sdk (TS) and the other server SDKs. Reports are POSTed fire-and-forget to /collect.

The grammar

import static ai.shipeasy.See.see;

try {
    chargeCard(order);
} catch (Exception e) {
    see(e)
        .causesThe("checkout")
        .to("use the backup processor", Map.of("order_id", order.id()));
}
  • see(problem) — start a report for a caught Throwable (or any object).
  • .causesThe(subject) — what part of the product is affected (default "app").
  • .to(outcome, extras)terminal. Builds the wire event and fire-and-forgets the send, with the extras folded in (structured context; merged on repeat, later wins). Calling .to() twice is a no-op; a chain that never calls .to() sends nothing.
  • .extras(Map) — standalone setter for the same context; reach for it only when you genuinely cannot pass the context inline.

Reporting never raises into your code — a failure in dispatch is swallowed and logged.

Where extras go in the chain

.causesThe(subject) and .to(outcome) are two halves of one sentence and must stay adjacent, so fold the extras into the terminal:

// PREFERRED — the consequence reads as one sentence:
see(e).causesThe("checkout").to("use cached prices", Map.of("order_id", oid));

.to() returns void, so extras cannot trail the terminal in Java — the call below does not compile. And never split the sentence with .extras():

// WON'T COMPILE — .to() returns void:
// see(e).causesThe("checkout").to("use cached prices").extras(Map.of("order_id", oid));

// WRONG — extras wedged between the subject and the outcome. You read
// "checkout … order_id … use cached prices" and lose the consequence.
// see(e).causesThe("checkout").extras(Map.of("order_id", oid)).to("use cached prices");

When the context already exists above the catch, prefer See.addExtras over the inline form — it keeps the catch site a clean one-liner.

Attach context from anywhere: See.addExtras

To attach context without threading it into the catch block, buffer it earlier in the request with See.addExtras. Every see() report that fires later on the same thread merges it in:

import ai.shipeasy.See;

// from any layer, early in the request
See.addExtras(Map.of("order_id", order.id(), "tenant", tenant.slug()));

// ...later, deep in a service...
try {
    charge(order);
} catch (Exception e) {
    See.see(e).causesThe("checkout").to("use cached prices");
    // report carries order_id + tenant automatically
}

The buffer is thread-local, so concurrent requests never bleed into each other, and it merges into every report in the request (not just the first). A chained .extras / inline .to extra of the same key overrides an ambient one. AnonIdFilter clears the buffer at the end of each request (register it like any servlet filter); in a background job or script call See.clearExtras() when a unit of work ends. See.addExtras works even before an engine is configured and never throws.

Dispatch

The static ai.shipeasy.See.see(...) dispatches against the engine that Shipeasy.configure(...) built — no handle to pass. A global call before configure runs logs a warning and returns a no-op chain — it never throws.

Violations (non-throwable problems)

Report a named problem that isn't an exception:

import static ai.shipeasy.See.violation;

violation("inventory_out_of_sync")
    .causesThe("fulfillment")
    .to("fall back to the nightly count");

Expected control flow (reports nothing)

Mark a throwable as expected control flow so it is not reported — only the mark is stamped on the throwable:

import static ai.shipeasy.See.controlFlowException;

try {
    return parse(input);
} catch (RetryableException e) {
    controlFlowException(e).because("the upstream told us to retry");
    throw e; // re-thrown; nothing is sent
}

ControlFlowChain.isExpected(throwable) lets callers query the mark. Any .extras() on the tail are local-debug only and never transmitted.

Limits

see() self-protects: messages/stacks are truncated, extras capped (20 keys, 200-char values), a 30s dedup window suppresses duplicates, and the per-process cap is 25 sends. Private attributes (see Advanced) are stripped from outbound extras.

Was this page helpful?
Updated July 25, 2026

On this page