A venue coordinator edits a loading-door instruction. Clicking Save starts a request. Before it finishes, the coordinator adds an access code. If the first response clears the warning unconditionally, the newer access code can be lost when the tab closes. The warning needs to follow the unsaved content, including changes made during a save.
Track the saved baseline, not whether a field is empty
Start with content the application has already loaded successfully:
<label for="door-note">Loading-door instruction</label>
<textarea id="door-note">Use the west loading door.</textarea>
<button id="save-note" type="button">Save instruction</button>
<p id="save-state" role="status">Saved</p>
In the same page, keep one stable listener function so it can be removed:
const note = document.getElementById('door-note');
const saveButton = document.getElementById('save-note');
const status = document.getElementById('save-state');
let savedText = note.value;
let warningAttached = false;
function warnBeforeLeaving(event) {
event.preventDefault();
event.returnValue = true;
}
function refreshUnsavedState() {
const dirty = note.value !== savedText;
if (dirty && !warningAttached) {
window.addEventListener('beforeunload', warnBeforeLeaving);
warningAttached = true;
} else if (!dirty && warningAttached) {
window.removeEventListener('beforeunload', warnBeforeLeaving);
warningAttached = false;
}
status.textContent = dirty ? 'Unsaved changes' : 'Saved';
}
note.addEventListener('input', refreshUnsavedState);
An empty textarea can itself be an unsaved edit when the original instruction was nonempty. Comparing with the saved baseline handles that case. If the coordinator types and then restores the original wording exactly, the warning can be removed without making a request.
For a structured document, compare the application's meaningful saved representation rather than this single textarea string. Record switching also needs to establish the correct baseline for the new record.
Clear only the changes the server acknowledged
Connect the Save button to your application's existing save operation:
saveButton.addEventListener('click', async () => {
if (saveButton.disabled) return;
const submittedText = note.value;
saveButton.disabled = true;
status.textContent = 'Saving instruction';
try {
await persistDoorInstruction(submittedText);
savedText = submittedText;
refreshUnsavedState();
} catch {
refreshUnsavedState();
status.textContent = 'Save failed. Keep this page open and retry.';
} finally {
saveButton.disabled = false;
}
});
persistDoorInstruction is an application function, not a browser API. It must reject on failure and resolve only after the intended record has been saved under the application's actual contract. The example serializes manual saves through one disabled button; additional autosave or other writers require their own ordering policy.
After the acknowledgement, savedText becomes the exact submitted snapshot. If the coordinator has added the access code meanwhile, note.value differs from that snapshot and the listener remains attached. Do not set the saved baseline to whatever happens to be in the textarea when an older response arrives.
Keep the warning separate from saving and recovery
Modern browsers require prior user interaction before showing the warning, and they display browser-controlled wording. Returning a custom sentence from an addEventListener callback does not let you customize that dialog. Test a real edit and real navigation rather than dispatching a synthetic event and expecting browser UI.
beforeunload is unreliable for mobile termination. A user can switch apps and later have the browser closed without the event firing. Do not start the only save inside the unload listener and assume the browser will wait for its promise.
The document's visibilitychange event provides an earlier opportunity to checkpoint application state when the page becomes hidden. That checkpoint still needs a deliberate storage and failure policy. It is not an acknowledged server save merely because the event fired. The browser recovery guide and server autosave guide explain those separate responsibilities.
Test leaving before and after acknowledgement
Edit the instruction and attempt navigation after interacting with the page. Cancel the browser warning and confirm the text remains. Then save successfully and navigate again without further edits; the listener should have been removed.
Repeat with a delayed save response: add the access code while the earlier instruction is saving, let that response finish, then attempt to leave. The newer text must remain unsaved. Finally, test a failed save and an edit that restores the original baseline.
Attach the listener only while needed. MDN notes that Firefox excludes pages with beforeunload listeners from its back/forward cache. More importantly for the coordinator, an unnecessary warning teaches people to dismiss it. Once the current instruction is actually saved, ordinary navigation should proceed without that warning.