> 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/beyond-apex/profiling/backend-calls.md).

# Backend Calls

Most LWC performance problems live in the round trip to the server. `timeBackendCall(methodName, apexCall)` is the workhorse for measuring that round trip. It logs the start of the call, waits for the promise, logs completion (with duration), and — if something goes wrong — logs the failure with the same duration baseline plus the exception.

Under the hood it returns a `PerformanceCallBuilder`, which lets you tune *how* errors are handled before the call is actually executed.

## Basic Usage

{% code title="productSearch.js" lineNumbers="true" %}

```javascript
import Triton from 'c/triton';
import searchProducts from '@salesforce/apex/ProductController.searchProducts';

export default class ProductSearch extends LightningElement {
    triton;
    products = [];

    connectedCallback() {
        this.triton = new Triton().bindToComponent('ProductSearch');
        this.triton.trackComponentLifecycle('connected');
    }

    async handleSearch() {
        const searchTerm = this.searchInput.value;

        try {
            const results = await this.triton.timeBackendCall(
                'searchProducts',
                () => searchProducts({ searchTerm })
            ).execute();

            this.products = results;
        } catch (error) {
            this.showErrorMessage('Search failed. Please try again.');
        }
    }
}
```

{% endcode %}

Three things to notice:

1. The Apex call is wrapped in an arrow function so Triton controls when it runs.
2. `methodName` ("searchProducts") is the label that ends up on the resulting log records. Use the real Apex method name so logs are easy to find.
3. `.execute()` is the trigger — until you call it, no logging or Apex invocation happens.

## The Four Error-Handling Scenarios

`PerformanceCallBuilder` exposes two modifiers that combine into four scenarios. The defaults (built-in logging + rethrow) match what 90% of code wants.

| Modifiers                                                | Error handler           | Rethrow | When to use                                                                            |
| -------------------------------------------------------- | ----------------------- | ------- | -------------------------------------------------------------------------------------- |
| `.execute()`                                             | Built-in (logs failure) | Yes     | Default. The standard try/catch pattern.                                               |
| `.withoutRethrow().execute()`                            | Built-in (logs failure) | No      | You want failures logged but don't need an exception in the calling code.              |
| `.withCustomErrorHandler(fn).execute()`                  | Custom only             | Yes     | You want full control over the side effects but the calling code still needs to react. |
| `.withCustomErrorHandler(fn).withoutRethrow().execute()` | Custom only             | No      | Fire-and-forget calls with custom telemetry/UX.                                        |

### 1. Default: log + rethrow

```javascript
try {
    const result = await this.triton.timeBackendCall(
        'processPayment',
        () => processPayment({ amount: 100, cardToken: 'token123' })
    ).execute();

    this.showPaymentSuccess(result);
} catch (error) {
    this.handlePaymentError(error);
}
```

The failure is automatically logged with the method name, duration, and exception, then re-thrown so your `catch` block runs as usual.

### 2. Built-in logging, no rethrow

```javascript
const result = await this.triton.timeBackendCall(
    'processPayment',
    () => processPayment({ amount: 100, cardToken: 'token123' })
)
.withoutRethrow()
.execute();

if (result) {
    this.showPaymentSuccess(result);
} else {
    this.showPaymentWarning('Payment could not be processed');
}
```

Useful when you want the call to fail silently from a control-flow perspective but still capture the failure log. A `null` return is your signal that something went wrong.

### 3. Custom handler + rethrow

```javascript
try {
    const result = await this.triton.timeBackendCall(
        'processPayment',
        () => processPayment({ amount: 100, cardToken: 'token123' })
    )
    .withCustomErrorHandler((error, context) => {
        this.sendToErrorTracking({
            method: context.methodName,
            duration: context.duration,
            error: error.message,
            component: 'PaymentProcessor'
        });
    })
    .execute();

    this.showPaymentSuccess(result);
} catch (error) {
    this.handlePaymentError(error);
}
```

Custom handlers receive `(error, context)` where `context` is `{ methodName, duration }` for backend calls. Use this when you want to enrich the failure with extra side effects (external monitoring, fancy toasts) but still need an exception to bubble.

{% hint style="warning" %}
A custom handler **replaces** the built-in failure log — it doesn't run alongside it. If you still want a Triton log on failure, call `this.triton.log(...)` from inside your handler.
{% endhint %}

### 4. Custom handler, no rethrow

```javascript
const result = await this.triton.timeBackendCall(
    'processPayment',
    () => processPayment({ amount: 100, cardToken: 'token123' })
)
.withCustomErrorHandler((error, context) => {
    this.showUserFriendlyPaymentError(error);
    this.logToExternalService('payment_failure', {
        duration: context.duration,
        method: context.methodName,
        error: error.message
    });
})
.withoutRethrow()
.execute();

if (result) {
    this.showPaymentSuccess(result);
}
```

The most "tolerant" mode — perfect for non-critical operations like notifications, where a failure shouldn't poison the rest of the flow.

## Parallel Calls

`timeBackendCall` returns a builder, and `.execute()` returns a regular `Promise`, so you can fan out calls and wait on them with `Promise.all`. Each call gets its own timing, start log, and completion log.

```javascript
async connectedCallback() {
    this.triton = new Triton().bindToComponent('AccountDashboard');
    this.triton.trackComponentLifecycle('connected');

    try {
        const [accountData, relatedRecords, userPreferences] = await Promise.all([
            this.triton.timeBackendCall('getAccountData',
                () => getAccountData({ accountId: this.recordId })).execute(),
            this.triton.timeBackendCall('getRelatedRecords',
                () => getRelatedRecords({ accountId: this.recordId })).execute(),
            this.triton.timeBackendCall('getUserPreferences',
                () => getUserPreferences({})).execute()
        ]);

        this.accountData = accountData;
        this.relatedRecords = relatedRecords;
    } catch (error) {
        console.error('Failed to load dashboard data:', error);
    }
}
```

This pattern is especially powerful for record pages — you instantly get a per-call breakdown of which Apex method is the slow one, instead of staring at a single aggregated load time.

## What Gets Logged

For a successful backend call, you get two log entries:

| Log        | Type           | Summary                                | Notable fields                      |
| ---------- | -------------- | -------------------------------------- | ----------------------------------- |
| Start      | `BACKEND_CALL` | `Backend call started: <methodName>`   | `action = <methodName>`             |
| Completion | `BACKEND_CALL` | `Backend call completed: <methodName>` | `duration`, `action = <methodName>` |

A failure produces the start log plus a `Backend call failed: <methodName>` entry with `duration`, the exception details, and the stack trace (unless you've supplied a custom handler).

## Best Practices

* Use the real Apex method name as `methodName` — searching and grouping logs by method is dramatically easier.
* Keep the arrow function passed to `timeBackendCall` thin. Anything you do *inside* it that isn't the Apex call gets folded into the duration measurement.
* Lean on `withoutRethrow()` for fire-and-forget side effects (analytics pings, optional enrichment) so a flaky non-critical call can't break your component.
* Combine with [User Interactions](/beyond-apex/profiling/user-interactions.md) when a single user action triggers multiple backend calls — you'll get a parent interaction record plus children for each call.
