Skip to content
Guides contents

GuidesSync & optimistic writes

Handling rejected writes

Let the client reconcile a rejected prediction, then show the failure and a useful next action to the user.

View as Markdown

The optimistic client predicts a mutation before the server responds. The server can reject it when validation, authorization, or a database rule fails.

This guide extends the manual synced-app quickstart. The client reconciles predictions with authoritative state. Your application reports the failure and preserves any input the user needs to correct.

Distinguish rejection from a failed request

The mutation API returns an outcome for each envelope. An outcome with accepted: false is a final rejection. Causes include invalid arguments, an authorizeMutation denial, a thrown server guard, or a deterministic database constraint violation.

A failed HTTP request or unavailable authority is different. The mutation queue retries failed batches with backoff and reports them through onMutationError. For example, an HTTP authentication failure may require the application to restore its session before a retry can succeed. Do not report every network failure as a permanent rejection.

If the initial browser prediction throws, the call itself fails and no mutation is enqueued. Catch that error at the action that invoked the mutator.

Show the rejection

Add these callbacks to the quickstart’s existing createRindleClient options:

onRejected: (envelope, reason) => {
  window.alert(`${envelope.name} was rejected: ${reason}`);
},
onMutationError: (error, attempt) => {
  console.error(`Mutation delivery failed, attempt ${attempt}`, error);
},

envelope identifies the attempted write with its client ID, mutation ID, name, and arguments. Use those arguments to restore a form, or associate the failure with an item in your application.

The HTTP outcome and the confirming subscription are separate messages. onRejected reports the outcome; do not assume the UI has already received the confirming stream update when this callback runs. The client removes the rejected prediction as it reconciles with that stream. Do not manually reverse the rows.

For a real UI, replace the alert with an error message or notification. A retry is a new user action through the named mutator, subject to the same access rules.

An accepted no-op is different

A mutator can return without writing. The server accepts that mutation and advances its confirmation position; onRejected does not fire.

The browser and server can read different data. A body might predict a write locally and then do nothing on the server. Reconciliation corrects that prediction even though the server accepted the mutation. Use a thrown error when the user needs an explicit failure reason.

The create-rindle starter includes a rejection demonstration: a server guard rejects a message containing spam, and the browser displays a notification. See Authorizing reads and writes for application access rules and The API server for server-only mutation guards.