remix-run/remix · error · Error

nodeHmrRuntimeUnavailableError

Error message

nodeHmrRuntimeUnavailableError

What it means

Importing node-hmr/runtime.node-hmr.ts immediately checks for the node-hmr runtime via globalThis; if absent it throws Error('The node-hmr/runtime API is only available when running inside node-hmr'). The module is designed to run only in child processes spawned by the node-hmr supervisor, so importing it elsewhere fails at import time.

Source

Thrown at packages/node-hmr/src/runtime.node-hmr.ts:9

import { emitServerReady, getNodeHmrRuntime } from './lib/runtime.ts'
import type { NodeHmrRuntimeApi } from './lib/runtime-api.ts'
import { nodeHmrRuntimeUnavailableError } from './lib/runtime-api.ts'

export type { BrowserHmrChannel } from './lib/browser-events.ts'

const maybeNodeHmrRuntime = getNodeHmrRuntime()
if (maybeNodeHmrRuntime === undefined) {
  throw new Error(nodeHmrRuntimeUnavailableError)
}
const nodeHmrRuntime = maybeNodeHmrRuntime

/**
 * Connects browser asset tooling in this child process to the browser HMR event stream and file
 * watcher owned by its `node-hmr` parent process.
 *
 * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
 * independent channel that must be closed when its owner shuts down. The returned promise rejects
 * when browser HMR is disabled for the runner.
 *
 * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
 * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
 * also runs without HMR supervision.
 *
 * @returns A child-scoped channel for watching browser source files and publishing HMR events.
 */
export const createBrowserHmrChannel: NodeHmrRuntimeApi['createBrowserHmrChannel'] =

View on GitHub (pinned to 9696913134)

Solutions

  1. Only import node-hmr/runtime from code that runs under the node-hmr supervisor (e.g. via register hooks it installs)
  2. Guard the import with await import() inside a check for the runtime global, so standalone execution can skip it
  3. Start your dev process with the node-hmr CLI entrypoint

Example fix

// before
import { createBrowserHmrChannel } from 'node-hmr/runtime' // throws at import outside node-hmr

// after
let nodeHmr = globalThis.__nodeHmrRuntime__
if (nodeHmr) {
  let { createBrowserHmrChannel } = await import('node-hmr/runtime')
  await createBrowserHmrChannel()
}
Defensive patterns

Strategy: fallback

Validate before calling

if (typeof globalThis !== 'undefined' && '__nodeHmrRuntime' in globalThis) {
  const runtime = await import('node-hmr/runtime')
  await runtime.createBrowserHmrChannel()
}

Type guard

const isNodeHmrChild = (): boolean =>
  typeof globalThis === 'object' && globalThis !== null && '__nodeHmrRuntime' in globalThis

Try / catch

try {
  await import('node-hmr/runtime')
} catch (error) {
  if (error instanceof Error && error.message.includes('only available when running inside node-hmr')) {
    // standalone execution — skip node-hmr features
  } else throw error
}

Prevention

When it happens

Trigger: import-ing 'node-hmr/runtime' (runtime.node-hmr.ts) in a process not launched under node-hmr — plain node scripts, tests, or servers started without the node-hmr CLI — so getNodeHmrRuntime() returns undefined.

Common situations: Build/test tooling that transitively bundles or imports the runtime module outside dev; running the dev server with node instead of node-hmr; version changes making the module eager where it was lazy before.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/3846b1354f7c286d. Report an issue: GitHub.