Error Handling

When a Rust function returns Result<T, E>, errors are propagated as RpcError on the client side. You can also return custom HTTP status codes for authorization or validation errors.

The RpcError class provides .status, .message, and .data for structured error handling.

RpcError API

PropertyTypeDescription
.statusnumberHTTP status code (e.g. 401, 500)
.messagestringHuman-readable error message
.dataunknownParsed JSON error body from server

Global onError Hook

Catch all errors at the client level. The onError callback receives an ErrorContext with the procedure name, attempt number, and whether the client will retry.

ts
// Global error handler via RpcClientConfig
const rpc = createRpcClient({
  baseUrl: '/api',
  onError: (ctx) => {
    console.error(`${ctx.procedure} failed (attempt ${ctx.attempt})`);
    if (ctx.willRetry) {
      console.log('Retrying...');
    }
  },
});

Reactive Error Handling

Framework wrappers expose isError and error state for conditional UI rendering.

ts
// Reactive error handling with createQuery / createMutation
const users = createQuery(rpc, 'list_users', () => page, {
  onError: (err) => {
    // err is RpcError
    if (err.status === 401) {
      redirectToLogin();
    }
  },
});

// Check error state in templates
// users.isError   — boolean
// users.error     — RpcError | undefined

Mutation Errors

Use mutateAsync with try/catch for fine-grained control, or onError callback for fire-and-forget style.

ts
// Mutation error handling with mutateAsync
const create = createMutation(rpc, 'create_order');

try {
  const order = await create.mutateAsync(orderInput);
} catch (e) {
  if (e instanceof RpcError) {
    switch (e.status) {
      case 400: showValidationErrors(e.data); break;
      case 409: showConflictMessage(); break;
      default:  showGenericError(e.message);
    }
  }
}

// Or fire-and-forget with callback
const create2 = createMutation(rpc, 'create_order', {
  onError: (err) => toast.error(err.message),
});
create2.mutate(orderInput);

Timeout & Abort

Timeouts and manual aborts throw a DOMException with name: 'AbortError', not RpcError.

ts
// Timeout and abort errors
const controller = new AbortController();

try {
  await rpc.query('slow_report', input, {
    timeout: 5000,
    signal: controller.signal,
  });
} catch (e) {
  if (e instanceof RpcError) {
    // Server returned an error status
  } else if (e instanceof DOMException && e.name === 'AbortError') {
    // Request was aborted (timeout or manual cancel)
  }
}

Secret — Protected Endpoint

This endpoint requires an Authorization header. Try calling it with and without a token to see error handling in action.

visit GitHub to learn more about metaxy