All posts
Engineering

Why it breaks only on the first visit: race conditions in UI data loading

A standard-length technical guide to diagnosing first-visit UI failures, stale subscriptions, async sequencing, and race conditions hidden by refresh.

10 June 2026·8 min read
Why it breaks only on the first visit: race conditions in UI data loading

The most misleading bug report

“It fails the first time I open the page, but works after refresh.”

This is one of the most misleading bug reports in frontend engineering because the application appears to fix itself. The user refreshes the page, the issue disappears, and the team is left with a bug that feels inconsistent, environment-specific, or impossible to reproduce.

But this pattern usually points to a timing problem.

The first visit is when the application has the least amount of prepared state. Route parameters may still be resolving. Authentication context may still be loading. Store data may be empty. Subscriptions may not be connected yet. Components may render before their dependencies are ready.

After refresh, cached state, reordered execution, or a second initialization path may hide the issue.

That is why first-visit bugs should be treated as reliability bugs, not minor UI glitches.

Why first-visit bugs are hard to debug

A first-visit failure often sits between multiple layers of frontend behavior:

  • component lifecycle timing
  • route initialization
  • state management
  • API request sequencing
  • cached data
  • subscription setup
  • loading and error-state handling

Each individual piece may look correct when inspected alone. The bug appears only when the pieces run in a specific order.

That makes these bugs hard to reproduce locally. A developer may already have warm browser state, an authenticated session, fast local APIs, or cached store data. The user, meanwhile, hits the cold-start path where nothing is ready yet.

The important question is not only “Why did it fail?”

The better question is: “Which assumption was only true after the first run?”

The most common signal: refresh fixes it

When refresh fixes a problem, it usually means one of three things happened:

  1. The first run missed a required event.
  2. The first render assumed data existed too early.
  3. The second run benefited from state created during the failed first run.

That last point is especially important. A failed first visit can still populate some cache, store, session, or browser state. The second visit then succeeds, not because the bug disappeared, but because the environment changed.

This is why first-visit bugs can feel random when they are actually deterministic under the right timing conditions.

What to instrument first

Before changing code, instrument the sequence.

For a first-visit bug, logs should show the order of key events:

  • route/component initialization
  • input availability
  • authentication or session readiness
  • subscription creation
  • request start
  • response arrival
  • state update
  • render path
  • error handling
  • subscription teardown

The goal is to build a timeline.

A useful timeline answers questions like:

  • Did the component request data before route parameters existed?
  • Did the response arrive before the subscription was created?
  • Did a destroy signal close a stream too early?
  • Did the first render try to read fields from undefined data?
  • Did loading state remain active after an exception?
  • Did cached state make the second run behave differently?

Without this timeline, it is easy to fix the symptom while leaving the race condition intact.

Root cause 1: the UI requests data before inputs are ready

The most common first-visit problem is an eager request.

A component initializes and immediately requests data, but one of its dependencies is not ready yet. The dependency might be a route parameter, selected entity, tenant context, user session, feature flag, or parent component output.

Example patterns include:

  • route params not resolved
  • parent component still loading
  • auth/session context not populated
  • selected workspace or tenant not available
  • configuration loaded asynchronously
  • feature state initialized after the child component

The fix is not simply “add a delay.” Delays make race conditions less visible, not more correct.

A better fix is to make readiness explicit.

The component should request data only after the required inputs exist. In reactive UI code, that often means combining streams carefully, filtering incomplete states, and triggering the request only when the required context is available.

Root cause 2: subscriptions are created too late

Another common issue is late subscription.

A request fires, an event emits, or a subject updates before the component has subscribed to it. If the stream does not replay the latest value, the component misses the event and waits forever.

This can happen when:

  • the subscription is created after the triggering action
  • a shared subject is used without replay behavior
  • initialization order differs between cold start and refresh
  • the event is emitted synchronously before the UI is ready
  • the component assumes a future emission will happen, but it already happened

The symptom is often a loading state that never completes.

The system did produce the data, but the component missed the notification.

The fix may involve changing initialization order, using a replayable state source where appropriate, or designing the flow so the component reads current state instead of depending only on a future event.

Root cause 3: premature unsubscription

The opposite issue can also happen: the subscription exists, but it is torn down too early.

A destroy signal, reset event, route reuse behavior, or shared stream lifecycle can terminate the subscription before the response arrives. This is especially subtle when the same stream is reused across components.

A “destroy” signal intended for one component can accidentally affect a shared stream. After that, future actions silently do nothing in the same session.

The refresh works because it creates a fresh instance of the stream.

This class of bug is dangerous because the UI does not always throw an error. It simply stops reacting.

To debug it, log both subscription creation and teardown. If you only log requests and responses, you may miss the fact that the component was no longer listening when the response arrived.

Root cause 4: the first render assumes data is present

Many first-visit bugs are caused by optimistic rendering.

The component assumes that arrays, objects, nested fields, or computed values exist during the first render. That assumption may be true after refresh because data is cached or arrives faster. It may fail on cold start.

Examples include:

  • reading the first item of an empty array
  • accessing nested properties before the parent exists
  • rendering chart data before the series is built
  • using default selections before options are loaded
  • calculating layout from undefined values
  • running transformations before API data is available

The fix is to model loading, empty, error, and ready states separately.

A spinner alone is not a state model. It is only a visual indicator. The code still needs to know what the application is allowed to render at each stage.

Root cause 5: cached state hides the defect

Caching is useful, but it can hide first-run defects.

If the first attempt partially fills a store, browser cache, or service-level state, the second attempt may succeed. That makes the bug appear to be fixed by refresh, but the refresh only changed the starting conditions.

This is why first-visit bugs should be tested with cold state:

  • clear browser cache
  • clear local/session storage
  • use a fresh login/session
  • disable or inspect store hydration
  • test slower network conditions
  • test with empty API responses
  • test with delayed dependencies

A race condition that disappears on a warm path is still a race condition.

The fix mindset

The best fixes for first-visit bugs make sequencing explicit and idempotent.

A robust fix should:

  • make initialization repeat-safe
  • wait for required inputs before requesting data
  • scope subscriptions to the right lifecycle
  • avoid destroying shared streams from local components
  • treat loading, empty, error, and ready as separate states
  • avoid side effects in constructors
  • prefer explicit initialization phases
  • ensure every async path reaches a terminal state

The goal is not to make the bug harder to reproduce. The goal is to make the UI correct under every valid timing order.

A practical debugging checklist

When a bug only happens on the first visit, I usually walk through this checklist:

  1. Reproduce with cold browser state.
  2. Slow the network to make timing visible.
  3. Log component lifecycle events.
  4. Log when route parameters and required inputs become available.
  5. Log when subscriptions are created.
  6. Log when subscriptions are destroyed.
  7. Log request and response timestamps.
  8. Log store updates or subject emissions.
  9. Check whether the first render assumes non-empty data.
  10. Verify that loading state always clears on success, empty result, and error.

This checklist turns a vague race condition into a concrete sequence of events.

Why this is a reliability problem

It is tempting to treat first-visit bugs as frontend annoyances because refresh usually works. But users should not have to know which workflows need a refresh.

A reliable UI should behave correctly on cold start, slow network, empty state, reconnect, retry, and refresh.

First-visit bugs break trust because they make the application feel inconsistent. The user cannot tell whether the data is wrong, the page is broken, or the system is still loading.

That uncertainty is the real cost.

Closing

First-visit bugs are rarely random. They are usually timing bugs, lifecycle bugs, or state-readiness bugs.

The fix is not to add a delay or rely on refresh. The fix is to make the application’s assumptions explicit:

  • what data is required
  • when it becomes available
  • who owns the subscription
  • when the request should fire
  • how each async path terminates
  • what the UI should render in every state

A frontend is reliable when it works not only on the happy path, but under every reasonable timing interleaving.

If a page only works after refresh, the refresh is not the solution. It is the clue.