How the CRA Development Server Handles Browser Refresh on Errors

Create React App uses a custom Webpack dev-server client that opens a WebSocket connection to receive compilation status messages, preferring hot module replacement (HMR) to preserve state but forcing a full window.location.reload() whenever HMR fails, runtime errors cannot be recovered by Fast Refresh, or static assets change.

During development, Create React App (CRA) runs a custom client-side handler located in packages/react-dev-utils/webpackHotDevClient.js that manages the browser's response to compilation events. Instead of blindly reloading on every file change, the development server communicates via WebSocket to intelligently decide between applying hot updates or performing a full browser refresh when errors occur. This mechanism balances development speed with UI consistency by attempting to preserve component state while ensuring error-free renders.

WebSocket Communication and Compilation Messages

The development server establishes a persistent WebSocket connection to the browser client. When Webpack finishes compilation, the server emits JSON messages describing the build state, which the client processes in connection.onmessage within webpackHotDevClient.js.

The client recognizes specific message types that determine whether to trigger HMR or a full reload:

  • hash – Signals a new compilation hash, stored for update availability checks
  • ok / still-ok – Indicates successful compilation with no errors
  • warnings – Successful compilation containing lint or build warnings
  • errors – Compilation failed with syntax or build errors
  • content-changed – Static assets or files outside the src directory changed

Hot Module Replacement vs. Full Page Reload

The decision between hot patching and hard reloading lives primarily in the tryApplyUpdates function (lines 45–78 of webpackHotDevClient.js). This function evaluates multiple conditions before falling back to window.location.reload().

Successful Compilation Flow

When the client receives ok or still-ok messages, it invokes handleSuccess(). If this is not the initial compilation (!isFirstCompilation), the client attempts HMR through tryApplyUpdates():

  1. Checks if module.hot exists and status is idle
  2. Verifies isUpdateAvailable() using the compilation hash
  3. Calls module.hot.check(true, callback) to fetch updated modules

If module.hot.check succeeds and Fast Refresh can accept the changes, the update applies without reloading. The error overlay dismisses via ErrorOverlay.dismissBuildError().

Compile-Time Error Handling

When the client receives an errors message, it invokes handleErrors() which:

  • Clears the console
  • Formats messages using formatWebpackMessages.js
  • Displays the first error in the react-error-overlay
  • Does not reload the browser

The client waits for the next successful compilation before attempting recovery. This preserves the error context for debugging while allowing the developer to fix issues without losing the current application state in the console.

Runtime Error Recovery

Runtime errors trigger hadRuntimeError = true via the error overlay's startReportingRuntimeErrors. On the next successful compile, tryApplyUpdates detects this flag and checks canAcceptErrors() to determine if Fast Refresh can safely recover the component tree.

If hadRuntimeError is true but Fast Refresh cannot accept the error state, the client forces window.location.reload() to ensure a clean execution environment.

Static Asset Changes

When the WebSocket receives content-changed, the client bypasses HMR entirely and calls window.location.reload() immediately. This handles changes to public/ assets, index.html, or other files outside the hot-reloadable module graph.

The Decision Logic in tryApplyUpdates

The core reload logic resides in tryApplyUpdates, which evaluates safety conditions before applying updates:

// packages/react-dev-utils/webpackHotDevClient.js
function tryApplyUpdates(onHotUpdateSuccess) {
  if (!module.hot) {
    // No HMR support → hard reload
    window.location.reload();
    return;
  }

  if (!isUpdateAvailable() || !canApplyUpdates()) return;

  module.hot.check(true, (err, updatedModules) => {
    const haveErrors = err || hadRuntimeError;
    const needsForcedReload = !err && !updatedModules;

    // If we cannot apply the update safely, fall back to reload
    if ((haveErrors && !canAcceptErrors()) || needsForcedReload) {
      window.location.reload();
      return;
    }

    // Successful hot update
    if (typeof onHotUpdateSuccess === 'function') onHotUpdateSuccess();

    // Handle queued updates
    if (isUpdateAvailable()) tryApplyUpdates();
  });
}

The function forces a reload when:

  • Webpack encounters an error during the check (err exists)
  • A runtime error occurred and Fast Refresh cannot accept it (hadRuntimeError && !canAcceptErrors())
  • No modules were updated (!updatedModules), indicating a potential inconsistency

Error Overlay Integration

The react-error-overlay package (located in packages/react-error-overlay/src/index.js) provides the visual error interface. It records runtime errors through ErrorOverlay.startReportingRuntimeErrors() and displays compile-time errors via ErrorOverlay.reportBuildError().

When handleErrors() processes compilation failures, it formats messages using formatWebpackMessages.js before passing them to the overlay. After a successful hot update, handleSuccess() calls ErrorOverlay.dismissBuildError() to clear the overlay without reloading the page.

Summary

  • CRA's webpackHotDevClient.js opens a WebSocket to receive real-time compilation status from the development server
  • The client prefers hot module replacement through tryApplyUpdates() to preserve application state
  • Compile errors display in the error overlay without triggering an immediate browser refresh
  • Runtime errors set hadRuntimeError = true, forcing a reload on the next successful compile if Fast Refresh cannot recover
  • Static asset changes trigger immediate window.location.reload() via the content-changed message handler
  • The reload decision depends on module.hot.check() results, error presence, and Fast Refresh capability checks in canAcceptErrors()

Frequently Asked Questions

Does CRA reload the browser on every code change?

No. Create React App attempts hot module replacement first. The browser only reloads if HMR is unavailable, the update cannot be applied safely, or you modify static assets outside the Webpack module graph. Successful JavaScript edits typically update components without losing state.

What happens if a runtime error occurs during development?

The react-error-overlay captures the error and sets hadRuntimeError = true. The browser does not reload immediately. On the next successful compilation, tryApplyUpdates checks canAcceptErrors() to determine if Fast Refresh can handle the recovery. If not, it forces window.location.reload() to ensure a clean state.

Why does the page reload when I edit certain files but not others?

Changes to JavaScript/TypeScript files in the src directory attempt HMR first. However, if you modify files in the public folder, index.html, or other static assets, the server sends a content-changed message. The client handles this by calling window.location.reload() immediately, as these changes cannot be hot-patched into the running application.

Can I disable the automatic browser refresh behavior in CRA?

The automatic refresh is integral to the webpackHotDevClient.js implementation and cannot be disabled through standard configuration options. The client automatically decides between HMR and full reload based on compilation health. To prevent reloads, you would need to eject and modify the tryApplyUpdates logic or handle WebSocket messages manually, though this is not recommended for standard development workflows.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →