XSCREENSAVER / 3D PIPES

[readonly] markdown buffer

An Error Contract for LWC, Apex, and Flow

Jun 12, 2026 · 5 min read

Salesforce errors arrive as Apex exceptions, LDS field errors, GraphQL arrays, Flow faults, network failures, and JavaScript exceptions.

Users still have one question: what happened, and what can I do now?

If every boundary invents an answer, components parse transport details, Flows branch on prose, and support receives “script-thrown exception”. I prefer one small application contract.

Codes are for decisions. Messages are for people. Technical detail belongs in logs.

Give expected failures a shape

An imperative Apex command can return a typed result for expected business outcomes.

public class OperationResult {
    @AuraEnabled public Boolean success;
    @AuraEnabled public String errorCode;
    @AuraEnabled public String message;
    @AuraEnabled public String fieldName;
    @AuraEnabled public Boolean retryable;
    @AuraEnabled public String correlationId;
}

ACCOUNT_ON_HOLD is a valid result from attempting an operation, not necessarily an exceptional failure of the method itself.

Messages can be clarified or translated. Automation should branch on stable domain codes such as PERMISSION_DENIED, ORDER_ALREADY_SUBMITTED, or DEPENDENCY_UNAVAILABLE, never a substring from user-facing copy.

Flow can receive the same fields and use Decisions for known outcomes. Reserve the fault path for an action that could not execute its contract.

Keep technical failures on the server

Raw exceptions may disclose object names, field names, query fragments, and IDs.

try {
    return OrderService.submit(request);
} catch (OrderService.BusinessException exceptionValue) {
    return failure(
        exceptionValue.code,
        exceptionValue.getMessage(),
        false,
        correlationId
    );
} catch (Exception exceptionValue) {
    ErrorLogger.capture(exceptionValue, correlationId);
    throw new AuraHandledException(
        'We could not submit the order. Reference: ' + correlationId
    );
}

The unexpected path gives support a searchable reference without giving the browser a stack trace.

Field-level failures should include field context so an LWC can place the message beside the relevant input. Operation-level failures may belong in a toast. One should not be forced into the other.

Normalise platform errors once

Not every failure comes through the application contract. Translate LDS, GraphQL, and JavaScript errors in one utility rather than copying optional chaining through every component.

export function toProblem(error) {
  if (typeof error?.body?.message === 'string') {
    return { code: 'REQUEST_FAILED', message: error.body.message };
  }

  if (typeof error?.message === 'string') {
    return { code: 'CLIENT_ERROR', message: error.message };
  }

  return { code: 'UNKNOWN_ERROR', message: 'Something went wrong.' };
}

Components can then focus on display and recovery rather than transport archaeology.

Mark an error retryable only when repeating the same safe operation may succeed without user correction. Validation and permission failures are not retryable. Neither is a command that might already have completed unless idempotency protects it.

End with a contract the caller can trust

Tests should assert the stable code, success state, context, and retry policy. Avoid pinning an entire paragraph unless the exact copy is a requirement.

One contract does not erase every Salesforce error shape. It gives the application one place to translate them and one vocabulary for failures it owns.

That means fewer mystery toasts, fewer Flows parsing prose, and failures that support can actually investigate.