Debugging Nightmares - Multi-Tab Madness in Production by Shifa Salheen on July 18, 2025 145 views

We had just gone live.Everyone was loving it, and feedback was overwhelmingly positive. UI felt smooth, flows were clicking, and things were working well for almost everyone.
So when a user reported:
“I logged in… and it’s just spinning & I can’t see the dashboard”
— We didn’t worry much.
We told them to clear the storage & it worked.
We moved on.
We thought they had done something wrong — maybe since they were accessing multiple roles, multiple environments, they might have corrupted IndexedDB somehow.
We even convinced ourselves:
“It’s just a one-off. We won’t face this again.”
But like a toxic ex, you thought you’d blocked…
It came back.
After a few days of silence — boom.
Same issue.
Another user.
This time, an escalation from someone important.
Just that quiet, spinning screen haunting us from far.
That’s when we knew:
This wasn’t a glitch. This was something deeper. Something holding on — even when it should’ve let go.
The Investigation Begins
We threw everything at it —
multi-tab chaos, role-switching acrobatics, relentless login/logout loops —
desperate to trigger the bug.
Minutes ticked by.
Then an hour.
Still nothing.
Login was smooth. No spinner. No glitch.
And with every successful login, our frustration grew.
Because if we couldn’t reproduce it… how would we fix it?
Then it happened.
Unfiltered. Unlogged. Undeniable.
“I SEE THE SPINNER! I SEE THE DAMN SPINNER!” – A teammate yelled across the room.
One of us had stumbled into the perfect storm, unknowingly triggering the exact sequence.
And even though it was a bug…
We celebrated.
Because, like closure after a messy breakup —
You can’t fix what you can’t face.
Finally, we had a starting point.
The Breakthrough Scenario
The spinner had finally shown itself.
Now came the next question:
What did we do?
We paused everything — no more guesses, no more distractions.
Traced back every tab, every click, every login.
Then the memory clicked.
We had opened three tabs.
- Tab 1: Logged in as ABC → dashboard loaded fine
- Tab 2: Opened a submenu from the dashboard → worked perfectly
- Tab 3: Opened another submenu → also fine
Then it happened:
We logged out of Tab 2, closed it, and logged back in as user XYZ in Tab 3.
Boom. There came the haunting Spinner.
That’s when it made sense.
The old session was still clinging to life in another tab —
and the new login couldn’t overwrite it cleanly.
Our IndexedDB write silently froze.
Once we had this breadcrumb trail, we tried it again, step by step.
And it worked.
Every single time.
Different machines, fresh browsers, private windows — didn’t matter.
The bug was reproducible. Reliably. Consistently.
We finally had the recipe for disaster.
Now, we could bake the fix.
The Root Cause: A Transaction That Never Finished
Tracing the login flow revealed something subtle but deadly.
On each login, we store translations (language labels) into IndexedDB for faster rendering.
The write logic was wrapped in this clean transaction block:

Everything looked fine.
But the transaction never completed.
Not because of a bug in our logic, but because IndexedDB was being quietly… possessive.
Somewhere in another tab, the connection request with Indexed DB was still open. And if you try writing in another tab, it blocks you.
And the dramatic IndexedDB won’t yell or throw an error. It just waits.
No error.
No onerror
. No logs in any of the indexed DB method hooks .
No rejection.
Just an unresolved promise — hanging in emotional limbo.
Like a conversation that never ended. Like someone who never really logged out of your life.
It Hit Us — This Was Like Postgres, But Silent
It felt familiar.
Imagine trying to DROP DATABASE
in PostgreSQL, while your Spring Boot app still has an open connection.
Postgres will shout:
ERROR: database is being accessed by other users
It protects you — warns you.
But IndexedDB?
It doesn’t scream. It just sits there and doesn’t allow you to write; every request goes into a blocked state.
You try to write. It quietly refuses. No exceptions. No retries.
The operation simply… waits. Forever.
And that’s what made this bug so cruel.
It wasn’t a crash.
It wasn’t a rejection.
It was a ghost lock — inherited from another tab —
haunting your login and leaving your app to spin in silence.
Why It Was So Hard to Catch
Because IndexedDB behaves like a quiet partner who doesn’t tell you they’re upset.
If another tab has a connection open, it won’t crash your app.
It’ll just let you wait forever for something that’s never coming.
And that’s exactly what happened
One tab had left its connection open.
Another tried to write.
And the new session got stuck, waiting for closure that would never arrive.
The Fix: Let Tabs Talk, and Let Go
We realized that tabs in our app were behaving like people in a messy breakup:
One wouldn’t let go. The other couldn’t move on.
So we introduced a BroadcastChannel
called 'db-control'
— our form of healthy communication.
🔊 Step 1: Register the Listener
Now, every tab listens.
When a session ends, we tell all tabs to let go.
📡 Step 2: Broadcast on Logout (and Idle Timeout)
When the user clicks logout, we send this:

Even if the user never clicks “Logout” —
We handle it behind the scenes, so no tab stays attached to an old connection.
🛡️ Step 3: Add a Fail-Safe to the Write
Sometimes, despite everything, someone holds on.
Maybe they didn’t click logout.
Maybe, they just closed the lid of their laptop and went to sleep —
leaving an open tab quietly, clutching the IndexedDB connection.
So we added a fail-safe.
Before writing anything to IndexedDB, we test whether it’s even writable.
If it’s locked, we broadcast a CLOSE_DB
signal and give it a moment to release.
Then we try again — gently, like asking for closure.

What This Fix Gave Us
- A reliable write mechanism that retries only if necessary
- A communication system across tabs to release stale locks
- Protection against idle tabs holding old sessions hostage
- The ability to move on, even when the user doesn’t
Because in modern apps, not every user behaves predictably.
And not every tab closes its heart — I mean, connection — when it should.
Final Thought
It looked like a simple spinner issue.
But behind that harmless twirl, we uncovered the silent pain of IndexedDB, how it quietly backstabs by holding a lock, offering no error, and leaving your app in limbo.
And just like in life:
Every tab needs to know when to let go.
You can’t keep holding on to a session that’s already expired.
You can’t keep blocking new requests because you’re stuck in old ones.
We didn’t just patch a bug.
We built a system that knows how to MOVE ON.
🎬 Outro — Debugging Nightmares: Season 2
We call this one:
Debugging Nightmares: Season 2 — The IndexedDB Spectre
This wasn’t a bug that screamed in logs or crashed the screen.
This was a ghost — haunting tabs, holding silent locks, never announcing itself.
It taught us something deeper:
The worst bugs don’t bring down your app.
They leave it standing, pretending everything’s fine — while blocking everything that matters.
Another chapter in the humbling, oddly poetic world of real-world engineering —
Where fixing a spinner means rewriting how your tabs say goodbye.
If Season 1 was chaos, you could see…
Season 2 was a betrayal you couldn’t.
✍️ Author’s Note
Some bugs crash loudly.
Others linger quietly, refusing to let go.
This one? It was a lesson in how hard it is to move on when something’s still holding a lock.
If you’ve ever chased a bug that ghosts your logs, stalls your flow,
and only shows up when everything else is clean…
You’re not alone.
Good luck, dev.
See you in Season 3.
Fun fact: While writing this blog in Notion… even Notion showed me a spinner and I could not move ahead for a while.
Seems like IndexedDB got offended.
