usememos/memos · error · ErrMemoMutationConflict
memo state changed
Error message
memo state changed
What it means
ErrMemoMutationConflict (store/memo_attachment.go:10) is an optimistic-concurrency sentinel returned by Store.ApplyMemoMutation. Inside one transaction, each DB driver (store/db/{sqlite,mysql,postgres}/memo_attachment.go) rechecks the memo's existence, content, and attachment bindings that the API layer validated earlier; if another writer changed them in between, the transaction aborts with this error instead of overwriting concurrent state. The v1 memo attachment service maps it to codes.FailedPrecondition (server/router/api/v1/memo_attachment_service.go:181).
Source
Thrown at store/memo_attachment.go:10
package store
import (
"context"
"errors"
)
// ErrMemoMutationConflict indicates that memo or attachment state changed
// after an API request prepared its mutation.
var ErrMemoMutationConflict = errors.New("memo state changed")
// MemoAttachmentBinding describes one attachment that should be bound to a
// memo. WasBoundToMemo distinguishes an existing binding from a new one so the
// driver can reject ownership transfers while preserving legacy rows already
// attached to the memo.
type MemoAttachmentBinding struct {
ID int32
UID string
UpdatedTs int64
WasBoundToMemo bool
}
// MemoMutation atomically updates a memo, its attachment bindings, and its
// reference relations. Removed attachment rows are detached in the transaction
// and are deleted from storage separately, so a storage failure remains
// retriable.
type MemoMutation struct {
MemoID int32View on GitHub (pinned to 14d757ce1f)
Solutions
- Retry the whole operation: re-fetch the memo and attachments, rebuild the mutation from fresh state, and re-apply (the design keeps storage deletions outside the transaction precisely so retries are safe).
- Check for the sentinel with errors.Is(err, store.ErrMemoMutationConflict) at the service boundary and surface codes.FailedPrecondition so clients know to refresh rather than blindly repeat.
- Serialize concurrent edits client-side (e.g. disable submit while an edit is in flight) to avoid lost-update races.
- If it persists for one row, inspect the wrap message ("memo no longer exists", "attachment X is no longer bound") to see which precondition broke.
Example fix
// before
if err := s.Store.ApplyMemoMutation(ctx, mutation); err != nil {
return status.Errorf(codes.Internal, "failed: %v", err)
}
// after
if err := s.Store.ApplyMemoMutation(ctx, mutation); err != nil {
if stderrors.Is(err, store.ErrMemoMutationConflict) {
return status.Errorf(codes.FailedPrecondition, "memo state changed: %v", err)
}
return status.Errorf(codes.Internal, "failed to apply memo mutation: %v", err)
} Defensive patterns
Strategy: retry
Validate before calling
// Re-read the memo and attachments immediately before building the mutation, // and include ExpectedMemoContent from that fresh read: // mutation.ExpectedMemoContent = memo.Content (fetched in the same request).
Type guard
func isMemoMutationConflict(err error) bool {
return errors.Is(err, store.ErrMemoMutationConflict)
} Try / catch
if err := s.Store.ApplyMemoMutation(ctx, mutation); err != nil {
if stderrors.Is(err, store.ErrMemoMutationConflict) {
// reload memo + attachments, rebuild mutation, retry once;
// surface FailedPrecondition to the client if the retry also conflicts
return status.Errorf(codes.FailedPrecondition, "memo state changed: %v", err)
}
return err
} Prevention
- Always compare errors with errors.Is against the sentinel, never string-matching "memo state changed".
- Fetch memo state and apply the mutation in the same request handler to shrink the race window.
- Keep detached-row deletion outside the transaction (as the current design does) so a retry after conflict is safe.
When it happens
Trigger: Two concurrent UpdateMemoWithAttachments / SetMemo calls on the same memo; deleting or rebinding an attachment (DetachAttachment, ownership transfer to another memo) between the request's validation read and ApplyMemoMutation; a client retrying a stale request built from an old memo snapshot; memo deleted by another session while the mutation is in flight.
Common situations: Multi-tab or multi-device editing of the same memo, automation/scripts racing with the web UI, mobile clients with offline queues replaying old updates, or background runners touching attachments concurrently with a user edit.
Related errors
AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15).
Data as JSON: /api/errors/45593ac32ec4fff2.
Report an issue: GitHub.