{"record":{"id":"c179498072200368","repo":"usememos/memos","slug":"memo-not-found","errorCode":null,"errorMessage":"memo not found","messagePattern":"memo not found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/router/api/v1/memo_update_helpers.go","lineNumber":19,"sourceCode":"package v1\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/pkg/errors\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) buildUpdatedMemoState(ctx context.Context, memoID int32) (*store.Memo, *store.Memo, *v1pb.Memo, error) {\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, nil, nil, errors.New(\"memo not found\")\n\t}\n\n\tmemoName := buildMemoName(memo.UID)\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &memoName,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to list reactions\")\n\t}\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to list attachments\")\n\t}\n\trelations, err := s.loadMemoRelations(ctx, memo)\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to load memo relations\")","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/server/router/api/v1/memo_update_helpers.go#L1-L37","documentation":"buildUpdatedMemoState loads the memo by ID before applying an update; if the store lookup returns no row, this error aborts the update flow. Callers typically translate it into a NotFound status, but as thrown it is a bare store-level miss (GetMemo succeeded, row absent).","triggerScenarios":"UpdateMemo with a name whose parsed ID does not exist (deleted memo, wrong UID, ID from another instance); SetMemoResources/SetMemoRelations on a memo deleted by another session between read and write; retrying an update after the memo was removed.","commonSituations":"Stale UI after a memo was deleted in another tab or by a webhook; client caching memo lists past deletion; concurrent multi-device editing.","solutions":["Re-fetch the memo (GetMemo) before editing; if it 404s, refresh the list and drop the stale row.","Handle the NotFound-equivalent error in the UI by showing 'memo no longer exists' and reloading.","Verify the memo name/UID in the request matches an existing resource on this instance."],"exampleFix":"// before\nawait memoClient.updateMemo({ memo: { name: memoName, content: newText } });\n\n// after\ntry {\n  await memoClient.updateMemo({ memo: { name: memoName, content: newText } });\n} catch (e) {\n  if (e.code === 'not_found') { await queryClient.invalidateQueries(['memos']); return; }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Confirm the memo still exists before editing\nconst m = await memoClient.getMemo({ name: memoName }).catch(() => null);\nif (!m) throw new Error('Memo no longer exists; refresh the list');","typeGuard":null,"tryCatchPattern":"try {\n  await memoClient.updateMemo({ memo });\n} catch (e) {\n  if (e.message.includes('memo not found') || e.code === 'not_found') {\n    queryClient.invalidateQueries({ queryKey: ['memos'] });\n    notify('This memo was deleted elsewhere');\n    return;\n  }\n  throw e;\n}","preventionTips":["Keep memo list caches short-lived or invalidate on window focus to catch cross-tab deletions.","Show a 'resource changed/deleted' path in edit UIs, not just a generic error.","Re-fetch before applying updates to long-open editors."],"tags":["memo","not-found","stale-state","update"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}