{"record":{"id":"cf9301ba3e94353c","repo":"odysseus-dev/odysseus","slug":"failed-to-save-note","errorCode":null,"errorMessage":"Failed to save note","messagePattern":"Failed to save note","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/notes.js","lineNumber":471,"sourceCode":"    const data = await res.json();\n    _notes = data.notes || data || [];\n  } catch (e) {\n    console.error('Failed to fetch notes:', e);\n    _notes = [];\n  } finally {\n    _loading = false;\n  }\n}\n\nasync function _saveNote(note) {\n  const method = note.id ? 'PUT' : 'POST';\n  const url = note.id ? `${API_BASE}/api/notes/${note.id}` : `${API_BASE}/api/notes`;\n  const res = await fetch(url, {\n    method, credentials: 'same-origin',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(note),\n  });\n  if (!res.ok) throw new Error('Failed to save note');\n  return await res.json();\n}\n\nasync function _deleteNoteApi(id) {\n  // v2 review — used to swallow 4xx/5xx silently. Throw so callers can\n  // distinguish success vs failure and toast accordingly.\n  const r = await fetch(`${API_BASE}/api/notes/${id}`, { method: 'DELETE', credentials: 'same-origin' });\n  if (!r.ok) throw new Error('HTTP ' + r.status);\n}\n\nasync function _patchNote(id, patch) {\n  const res = await fetch(`${API_BASE}/api/notes/${id}`, {\n    method: 'PUT', credentials: 'same-origin',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(patch),\n  });\n  if (!res.ok) throw new Error('Failed to update note');\n  return await res.json();","sourceCodeStart":453,"sourceCodeEnd":489,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/notes.js#L453-L489","documentation":"Thrown by _saveNote in notes.js when PUT /api/notes/{id} or POST /api/notes returns non-ok. Unlike its siblings it discards the response body entirely, giving a fixed message with no status or server detail. Callers use it to decide whether to toast a save failure.","triggerScenarios":"Saving a note whose id no longer exists server-side (404, e.g. cleared store or another device deleted it); sending a body that fails note schema validation (400); auth cookie expired (401).","commonSituations":"Notes stored in a file/database wiped between sessions while the UI still held stale ids; concurrent editors; server restarted with a fresh data store.","solutions":["Reload the notes list to resync ids, then re-apply the edit and save.","If validation, check required fields (title/body/types) against the note schema.","Include the status code in the message (see exampleFix) to make recurrences diagnosable.","Re-authenticate if the session expired."],"exampleFix":"// before\nif (!res.ok) throw new Error('Failed to save note');\n\n// after\nif (!res.ok) throw new Error(`Failed to save note (HTTP ${res.status})`);","handlingStrategy":"try-catch","validationCode":"if (note.id && !/^[a-z0-9-]+$/i.test(String(note.id))) note.id = undefined; // force create when id looks stale","typeGuard":"const isNotePayload = (n) => n && typeof n.body === 'string';","tryCatchPattern":"try { return await _saveNote(note); } catch (e) { if (/HTTP 40[04]/.test(e.message)) { delete note.id; return await _saveNote(note); } throw e; }","preventionTips":["Include the HTTP status in thrown messages","On 404, retry as a create (id removed)","Re-sync note ids when the list reloads"],"tags":["notes","http","crud","frontend"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}