windmill-labs/windmill · error

Saving the latest draft failed (${failureMessage ?? 'unknown

Error message

Saving the latest draft failed (${failureMessage ?? 'unknown error'}). Retry before opening a session.

What it means

After flushing, flushOrRefuse inspects the syncer's persist state. If state is 'failed', it throws with the syncer's failureMessage, telling the user the latest draft is not durably saved and must be successfully saved before a session can open on it.

Source

Thrown at frontend/src/lib/components/sessions/pageDrawerSession.ts:60

// the user is looking at still holds the edit. The flush is the explicit kind, saving even
// with auto-save off: asking for a session is asking for the edit to come along.
async function flushOrRefuse(query: Parameters<typeof UserDraftDbSyncer.flush>[0]): Promise<void> {
	// Before the save, not after: text that does not parse never reached the draft, so
	// leaving now would open the session on the last value that did and drop the buffer
	// with the drawer — and saving first would write that stale value on the way to
	// refusing. The editors were materialised before this call, so the check is current.
	if (anyEditorUnparseable()) {
		throw new Error('This page has changes that are not valid JSON. Fix them first.')
	}
	await UserDraftDbSyncer.flush(query)
	if (UserDraftDbSyncer.getConflict(query).conflict) {
		throw new Error(
			'This draft has a newer conflicting version on the server. Resolve it here before opening a session.'
		)
	}
	const { state, failureMessage } = UserDraftDbSyncer.getState(query)
	if (state === 'failed') {
		throw new Error(
			`Saving the latest draft failed (${failureMessage ?? 'unknown error'}). Retry before opening a session.`
		)
	}
}

// How each page addresses a row in its hash. Resources route theirs through an extra
// segment; every other page names the path directly.
const drawerHashFor = (pagePath: string, itemPath: string) =>
	pagePath === RESOURCES_PATH ? `/resource/${itemPath}` : itemPath

/**
 * Deep-link the row whose drawer just opened, so the location says what is on screen — a
 * drawer opened from a row's Edit button is as open as one reached by link, and the chat
 * is told what the session shows through the location alone. No-op off that page, where
 * these drawers also open inside editors and pickers with no row convention to keep.
 *
 * Written straight to history rather than through the router: these pages open their
 * drawer *from* the hash, so a router-visible write would come back as a second open on

View on GitHub (pinned to e474e8803c)

Solutions

  1. Retry the save (fix the underlying network/auth issue) until state is success, then open the session
  2. Check the reported failureMessage for the root cause (401 → re-login, 5xx → server logs)
  3. Reload the page after connectivity is restored and re-flush
Defensive patterns

Strategy: retry

Validate before calling

const { state } = UserDraftDbSyncer.getState(query)
if (state === 'failed') return; // don't open a session on an unsaved draft

Type guard

function isPersisted(state: string): boolean { return state === 'success' }

Try / catch

try { await openSession(query) } catch (e) { if (/Saving the latest draft failed/.test(String(e))) scheduleRetryFlush(); else throw e }

Prevention

When it happens

Trigger: flushOrRefuse(query) where UserDraftDbSyncer.getState(query).state === 'failed' — the flush request to persist the draft errored (network down, 4xx/5xx, auth expired).

Common situations: Working offline or on a flaky connection; API returning 500 during draft save; expired session/token rejecting the draft write.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/9969f7b57f22354d. Report an issue: GitHub.