When "Cancel" Wasn't Actually Canceling

Debugging · May 08, 2026 · 4 min read

Canceling a modal should be a zero-side-effect action. But a well-intentioned auto-save feature turned a simple dismiss click into a ghost data nightmare.

This was one of the most deceptive bugs I ran into while building the CIVA admin dashboard.

I was testing the product creation wizard: I opened the modal, selected a category, typed a test product title, and changed my mind. I hit the "Cancel" button. The modal closed cleanly.

A minute later, I opened the system audit log. Right at the top:

Admin User updated draft "Test Motherboard" (Status: Pending)

Even worse, on the main table, a ghost product row with half-filled attributes was sitting there until I hard-refreshed the browser.

Cancel hadn't canceled anything. It had silently committed dirty state behind my back.


What Actually Happened

When building complex forms, developers often try to be helpful. I had written a helper to prevent users from losing work if they accidentally closed a modal:

// The well-intentioned trap
const handleClose = () => {
  if (isDirty) {
    syncDraftToBackend(formData); // "Helpful" auto-save
  }
  onClose();
};

The fatal flaw was using handleClose for both the explicit "Save & Exit" button and the "Cancel" button (as well as the Escape key listener).

By conflating "dismissing without saving" with "safely preserving uncommitted drafts", any half-typed junk entered into the modal was treated as a valid draft update.


The Realization: Explicit Intent Must Be Respected

An interface should never guess whether a user wants to discard or save. When someone clicks Cancel, their intent is unambiguous: throw this away.

I split the modal lifecycle into three distinct handlers:

  1. handleCancel(): Explicitly wipes local state, aborts in-flight network requests, and triggers zero backend mutations.
  2. handleSaveDraft(): An explicit, dedicated action that validates the bare minimum schema before syncing.
  3. handleCommit(): The final submission step with complete field validation.
// Clean, predictable separation
const handleCancel = () => {
  resetFormState();
  abortPendingRequests();
  onClose();
};

The Takeaway

Defensive UX—like auto-saving uncommitted work—must never override explicit user commands.

When a user clicks Cancel, the system should behave as if the interaction never took place. No network calls, no audit entries, no residual cache. True simplicity means predictable side effects.

— Abu Bakar Hasan

If this resonated, let's talk. Get in touch →