> For the complete documentation index, see [llms.txt](https://triton.pharos.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://triton.pharos.ai/triton-best-practices/exception-handling.md).

# Exception Handling

Turning caught exceptions into actionable, correlated error logs — the one-call pattern, partial-DML results, and when to publish now or create an issue.

Error logging is where a logging framework earns its keep. When something breaks in production, the quality of your exception logs decides whether the fix takes five minutes or five hours. Triton makes doing it right almost effortless — the trick is knowing the one call that does the work, and the handful of judgment calls around it.

## The entire pattern: `.exception(e)`

You almost never hand-assemble an error log. `.exception(e)` does all of it in one call — it pulls the **type** from the exception (falling back to `Backend`), sets the level to **ERROR**, copies the message into the **summary**, captures the **stack trace** and details, and **flags the log for issue creation**.

{% code lineNumbers="true" %}

```apex
try {
    chargeCard(payment);
} catch (Exception e) {
    Triton.logNow(Triton.t.area(TritonTypes.Area.OpportunityManagement).exception(e));
    throw e;
}
```

{% endcode %}

That's it. Set the area (or let a template do it), pass the exception, publish it, rethrow. Everything else is handled.

{% hint style="info" %}
**Don't spell out what `.exception(e)` already does.** Setting `.level(ERROR)`, `.type(...)`, `.summary(e.getMessage())`, and `.stackTrace(...)` by hand is redundant — and easy to get subtly wrong. Pass the exception and let the builder do it. If you want to add context, chain `.details(...)` or `.relatedObject(...)` *in addition*.
{% endhint %}

## Buffer or publish?

For errors, the answer is usually **publish immediately with `logNow`** — because the very next line is often a `throw` that ends the transaction and would take a buffered log down with it.

* **`Triton.logNow(Triton.t.exception(e))`** — the default for a real failure. Persisted before anything else can go wrong.
* **`Triton.error(Triton.t.exception(e))`** — buffers at ERROR. Fine when you're confident a `flush()` will run (e.g. a batch chunk's `finally`) and you're batching several logs together.

When in doubt, `logNow`. A lost error log is the worst kind to lose.

## Add context, then get out of the way

`.exception(e)` captures the technical picture; you add the business picture:

{% code lineNumbers="true" %}

```apex
try {
    updateAccount(accountId, payload);
} catch (Exception e) {
    Triton.logNow(Triton.t
        .area(TritonTypes.Area.Accounts)
        .exception(e)
        .relatedObject(accountId)          // which record
        .details('operation=update'));     // what we were doing
    throw e;
}
```

{% endcode %}

## Catch specifically when you'll act differently

If different exception types warrant different handling, catch them separately — but the logging call is the same shape each time. Let the exception's own type flow through; don't override it.

{% code lineNumbers="true" %}

```apex
try {
    processRecord(recordId);
} catch (DmlException e) {
    Triton.logNow(Triton.t.area(TritonTypes.Area.Accounts).exception(e).relatedObject(recordId));
    // ...DML-specific recovery...
} catch (CalloutException e) {
    Triton.logNow(Triton.t.area(TritonTypes.Area.RestAPI).exception(e).relatedObject(recordId));
    // ...retry / fallback...
} catch (Exception e) {
    Triton.logNow(Triton.t.area(TritonTypes.Area.Accounts).exception(e).relatedObject(recordId));
    throw e;
}
```

{% endcode %}

Note the Type field will read `DmlException`, `CalloutException`, etc. automatically — that's `.exception(e)` doing its job, and it makes filtering by failure kind trivial.

## Partial DML: use `.dmlResults(...)`, not a hand-rolled loop

For allow-partial DML (`Database.insert(records, false)`), don't loop over the results assembling a failure string. Pass the results straight to the builder — it tallies successes and failures, collects the failed ids as related objects, formats the messages, sets Type to `DMLResult`, and drops to ERROR level automatically. It only logs when there's actually a failure.

{% code lineNumbers="true" %}

```apex
List<Database.SaveResult> results = Database.insert(accounts, false);
Triton.warning(Triton.t
    .area(TritonTypes.Area.Accounts)
    .summary('Bulk account insert completed with errors')
    .dmlResults(results));
```

{% endcode %}

It understands `SaveResult`, `DeleteResult`, `UpsertResult`, `UndeleteResult`, and `MergeResult`.

## Errors around callouts

Attach the sanitized payload so you can see what actually went over the wire:

{% code lineNumbers="true" %}

```apex
HttpRequest req = buildRequest(endpoint, payload);
try {
    HttpResponse res = new Http().send(req);
    if (res.getStatusCode() != 200) {
        Triton.logNow(Triton.t
            .area(TritonTypes.Area.RestAPI)
            .warning()
            .summary('Callout returned ' + res.getStatusCode())
            .integrationPayload(req, res));   // sanitize first
    }
} catch (CalloutException e) {
    Triton.logNow(Triton.t
        .area(TritonTypes.Area.RestAPI)
        .exception(e)
        .integrationPayload(req, null));
    throw e;
}
```

{% endcode %}

## Issue creation — deliberate, not automatic-everywhere

`.exception(e)` and `.error()` flag the log for **Pharos Issue** creation, which is exactly right for genuine failures a human should triage. But that's a double-edged sword: flag every recoverable hiccup and you get alert fatigue, and real incidents drown in noise.

{% hint style="warning" %}
**Reserve issue creation for things worth waking someone for.** Genuine, user-impacting failures → `ERROR` via `.exception(e)` (issue flagged, as you want). Recoverable, expected conditions (a retry that succeeded, a fallback path) → `WARNING`, which does **not** flag an issue. You can also opt in explicitly with `.createIssue()` when you build a non-exception log that still deserves attention.
{% endhint %}

## LWC exceptions

Same idea, LWC logger. `triton.exception(error)` builds an error log from a JS `Error`; publish it immediately with `logNow`.

{% code lineNumbers="true" %}

```javascript
async handleSave() {
    try {
        const result = await saveRecord({ recordId: this.recordId, data: this.formData });
        this.triton.log(this.triton.fromTemplate().summary('Record saved').relatedObjects([result.Id]));
    } catch (error) {
        await this.triton.logNow(
            this.triton.fromTemplate()
                .exception(error)
                .summary('Save failed')
        );
        this.showError('Something went wrong. Support has been notified.');
    }
}
```

{% endcode %}

Wire errors deserve the same treatment — log the `error` branch of every `@wire` callback. See [Beyond Apex → LWC](/beyond-apex/lwc.md).

## Don't swallow

Logging an exception is not the same as handling it. If the design doesn't genuinely recover from the failure, **rethrow after logging** — otherwise you've hidden a broken operation behind a log entry, and the caller carries on as if nothing happened.

{% code lineNumbers="true" %}

```apex
} catch (Exception e) {
    Triton.logNow(Triton.t.exception(e));
    throw e;   // don't leave this out unless recovery is intentional
}
```

{% endcode %}

## Related reading

* What `.exception(e)` and `.dmlResults(...)` set, field by field: [Core Concepts → The Builder & Templates](/core-concepts/builder-and-templates.md).
* Why errors go out with `logNow`: [Buffering & Flushing](/triton-best-practices/buffering-and-flushing.md).
