Cancel Is Not an Error
How a simple UX fix taught me about data integrity, user trust, and the difference between aborting and completing.

I was building a book scanning feature for my personal library app. The flow was straightforward: scan a barcode, fetch metadata from Google Books, confirm you want to add it, pick a bookcase, pick a shelf, done.
It worked great—until I hit Ctrl+C halfway through.
I'd selected a bookcase but hadn't picked a shelf yet. When I checked the database, there it was: a book with no location. An orphan. A record that shouldn't exist.
This is the story of how I fixed it, and what it taught me about a principle I now think about constantly: cancel is not an error.
The Trap
Here's what the flow looked like:
? Would you like to add this book to the library?
> Yes — Let's Do It
? Choose a Bookcase:
Bookcase 1
Bookcase 2
Notice anything missing?
There's no way out. If you've confirmed you want to add the book, you're committed. The only escape is Ctrl+C—and depending on where in the flow you abort, you might leave garbage in your database.
I'd violated an invariant I hadn't even articulated yet:
If the user cancels, nothing should change.
This seems obvious in retrospect. But when you're building the happy path, it's easy to forget that users change their minds. They realize they scanned the wrong book. They remember they already own it. They get a phone call. Life happens.
And when life happens mid-wizard, your system needs to handle it gracefully.
The Fix (The Easy Part)
The mechanical fix was simple: add a [Cancel] option to every selection prompt.
? Choose a Bookcase:
[Cancel]
Bookcase 1
Bookcase 2
? Choose a Shelf in "Bookcase 1":
[Cancel]
Shelf 1
Shelf 2
If the user selects Cancel, the flow exits with a friendly message:
Canceled. No changes were made.
No stack trace. No error styling. No guilt. Just a clean exit.
The Fix (The Hard Part)
But adding the Cancel button wasn't enough. I had to restructure when the book gets saved.
My original flow wrote to the database incrementally. Confirm the book? Create a record. Pick a bookcase? Update the record. Pick a shelf? Update again.
This is a classic mistake. It's convenient for the developer—you can persist state as you go—but it violates a fundamental principle:
Don't persist until you have everything you need.
I refactored to an all-or-nothing model:
✅ ISBN lookup succeeds → hold in memory
✅ User confirms → still in memory
✅ User picks bookcase → still in memory
✅ User picks shelf → now persist
Cancel at any step? Nothing to clean up. The data never hit the database.
This pattern has a name in transaction processing: atomicity. Either all of it happens, or none of it does. I wasn't thinking about ACID properties when I built the happy path, but that's exactly what I needed.
Cancel as a First-Class Outcome
Here's the mental shift that stuck with me: cancel is not an error.
It's not exceptional. It's not a failure. It's a valid, expected user action—as legitimate as completing the flow successfully.
This has implications:
Messaging. "Canceled. No changes were made." is very different from "Error: operation aborted." One respects the user's choice. The other makes them feel like they did something wrong.
Exit codes. If you're building CLI tools, cancel should return exit code 0 (success), not 1 (failure). This matters for scripting and automation. A user choosing not to proceed isn't a failure condition.
Testing. You need tests that verify the absence of side effects, not just the presence of expected outcomes. "Cancel at step 3 and confirm no database writes" is as important as "Complete flow and confirm book exists."
The Bigger Lesson
This was a 30-minute fix. Add some menu options, restructure persistence, update messaging. But it crystallized something I think about all the time now:
Every multi-step flow is a contract. You're asking the user to commit time and attention. In return, you owe them:
A clear exit at every step
Zero side effects if they leave early
Messaging that respects their choice
The first one is UX. The second one is engineering. The third one is trust.
Get all three right, and your software feels safe to use. Users will explore more, try things they're not sure about, and recover gracefully when they change their minds.
Get them wrong, and your software feels like a trap. Users will hesitate, second-guess, and resent you when they inevitably need to back out.
Coda
I shipped the fix. Now when I scan a book and realize I grabbed the wrong one, I hit Cancel and try again. No orphan records. No cleanup. No friction.
It's a small thing. But small things compound. And "cancel works correctly" is the kind of invisible quality that separates software you tolerate from software you trust.
I'm building Bibby, a personal library management CLI, as a learning project while transitioning into software engineering. I write about what I learn along the way.




