The code compiles.
The tests pass.
The feature works perfectly on a developer’s machine.
Then it reaches production and breaks.
This happens because production software is more than the code visible in one file. It runs inside browsers, sessions, APIs, cached data, feature flags, third-party services, and user workflows happening at unpredictable times.
Code can be correct in isolation while the complete system is still wrong.
Correct code still depends on assumptions
Consider this calculation:
const total = order.items.reduce(…)
Nothing is obviously wrong with it.
It works when:
The order exists
items exists and is an array
Every item contains a valid price
The order has finished loading
The application is using the expected data structure
The same code fails when one of those assumptions becomes false.
The calculation may be correct for the data it expects. Production breaks because the system provides something different.
That is why debugging should not stop with:
Which line threw the exception?
A stronger question is:
What assumption became false before this line executed?
Applications constantly rely on assumptions that are rarely written down. A component may assume the user is authenticated, a request succeeds, a route parameter exists, stored data uses the latest format, or a third-party service is available.
Most assumptions are reasonable during normal operation.
Production creates the unusual conditions.
The happy path is not the only path
Development usually tests the intended workflow:
The user opens the page.
The request succeeds.
The data loads.
The user submits the form.
The server responds.
The success message appears.
Real users create many other paths:
They submit twice
They navigate away during a request
Their session expires
They restore old cached data
They switch accounts while data loads
Their connection becomes unstable
A third-party script is blocked
They keep the application open through a deployment
A feature may work perfectly on the happy path while remaining unreliable everywhere around it.
A useful investigation asks which path the affected user actually followed.
Timing can break valid operations
Many production bugs are timing problems.
Imagine this sequence:
The application begins loading a profile.
The user switches accounts.
A second request begins.
The second request finishes first.
The first request finishes later.
The older response replaces the newer profile.
Both requests succeeded.
Both response handlers behaved as written.
The application still ends in the wrong state.
This race condition may disappear locally because the network is fast and requests finish in the expected order.
Useful debugging context should show when each request began, when each response arrived, which account initiated it, and which response updated the interface.
Without that timeline, the final incorrect state can look completely mysterious.
The visible exception may begin with a failed request
Frontend code is often developed using predictable API data.
Production APIs can return:
Unauthorised responses
Timeouts
Empty bodies
Partial data
Older schemas
Rate-limit errors
Successful responses containing invalid content
A common failure pattern looks like this:
A request fails.
The application records the failure.
The workflow continues as though valid data exists.
A later component throws an unrelated-looking exception.
The visible error might say:
Cannot read properties of undefined.
But the real problem may be that the application continued after the request failed.
The stack trace shows where the missing value became visible. The request history explains why it was missing.
Defensive code can hide the defect
A common response to an undefined value is optional chaining:
order?.items?.reduce(…)
Sometimes that is correct.
But if every valid order must contain items, the change may hide invalid state and allow checkout to continue incorrectly.
Possible consequences include:
Wrong totals
Incomplete payments
Empty confirmation pages
Corrupted requests
Harder debugging later
Defensive code is useful when the missing value is expected and the product has a defined fallback.
It is dangerous when it hides a condition the system should never allow.
The question is not only:
How do we stop this line from throwing?
It is:
What should the product do when this value is unavailable?
Cached data creates version problems
Applications change over time.
Stored data may not.
A new release may expect:
cart.items
while an older release stored:
cart.products
New sessions work correctly. Sessions created before deployment fail.
The current code looks valid because it matches the latest structure. The bug exists in the transition between old and new data.
This can affect local storage, cookies, persisted state, service-worker caches, and browser tabs left open during a deployment.
When only some users are affected, compare:
When their session was created
Which release stored the data
Which release read it
Whether clearing storage fixes the issue
Whether successful users have newer state
Tests only cover the conditions they contain
Automated tests are essential, but they cannot prove that no bugs exist.
A test confirms that the application behaved correctly under the conditions represented by that test.
Production failures often involve conditions that were never tested:
Older stored data
Requests completing out of order
Expired sessions
Rapid repeated clicks
Partial API responses
Slow third-party services
Multiple active tabs
Feature-flag combinations
When a production bug appears, ask which condition was missing from the tests and whether it should become a regression test.
Investigate the complete sequence
Begin with what the user experienced:
What they attempted
What they expected
What happened instead
Which browser, release, and workflow were involved
Then reconstruct the sequence:
User actions
Requests and responses
State updates
Authentication changes
Route transitions
Feature flags
Third-party activity
Look for the first abnormal event—not only the final exception.
Compare failed sessions with successful ones and form a testable explanation.
The strongest fix usually belongs at the earliest point where the system allowed invalid state to continue.
After deployment, validate the complete workflow. Confirm that users recover, the original error disappears, no replacement error appears, and older sessions work safely.
The final lesson
Code does not run in isolation.
It runs inside browsers, sessions, networks, releases, APIs, state transitions, feature flags, and third-party systems.
A function can look correct and still participate in a broken workflow.
When production fails, do not inspect only the line that threw.
Inspect the assumptions and events that allowed the system to reach that line.
The code may look correct.
The context explains why the app still broke.





