odysseus-dev/odysseus · error · Error
Failed to save note
Error message
Failed to save note
What it means
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.
Source
Thrown at static/js/notes.js:471
const data = await res.json();
_notes = data.notes || data || [];
} catch (e) {
console.error('Failed to fetch notes:', e);
_notes = [];
} finally {
_loading = false;
}
}
async function _saveNote(note) {
const method = note.id ? 'PUT' : 'POST';
const url = note.id ? `${API_BASE}/api/notes/${note.id}` : `${API_BASE}/api/notes`;
const res = await fetch(url, {
method, credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(note),
});
if (!res.ok) throw new Error('Failed to save note');
return await res.json();
}
async function _deleteNoteApi(id) {
// v2 review — used to swallow 4xx/5xx silently. Throw so callers can
// distinguish success vs failure and toast accordingly.
const r = await fetch(`${API_BASE}/api/notes/${id}`, { method: 'DELETE', credentials: 'same-origin' });
if (!r.ok) throw new Error('HTTP ' + r.status);
}
async function _patchNote(id, patch) {
const res = await fetch(`${API_BASE}/api/notes/${id}`, {
method: 'PUT', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
if (!res.ok) throw new Error('Failed to update note');
return await res.json();View on GitHub (pinned to f9235ebbf1)
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.
Example fix
// before
if (!res.ok) throw new Error('Failed to save note');
// after
if (!res.ok) throw new Error(`Failed to save note (HTTP ${res.status})`); Defensive patterns
Strategy: try-catch
Validate before calling
if (note.id && !/^[a-z0-9-]+$/i.test(String(note.id))) note.id = undefined; // force create when id looks stale
Type guard
const isNotePayload = (n) => n && typeof n.body === 'string';
Try / catch
try { return await _saveNote(note); } catch (e) { if (/HTTP 40[04]/.test(e.message)) { delete note.id; return await _saveNote(note); } throw e; } Prevention
- Include the HTTP status in thrown messages
- On 404, retry as a create (id removed)
- Re-sync note ids when the list reloads
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- HTTP ${r.status}
- Failed to trigger task (${res.status})
- Failed to stop task (${res.status})
- Failed to revert task
- HTTP ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/cf9301ba3e94353c.
Report an issue: GitHub.