remix-run/remix · error · TypeError

hmr must create a channel with a close function

Error message

hmr must create a channel with a close function

What it means

Part of the HMR channel contract validation in the asset server. When the `hmr` option's `createChannel` hook returns a channel object, Remix verifies it exposes the required interface (`url`, `close`, `onFileEvents`, `updateWatchedFiles`). This error means the returned channel is missing the `close` function, so the dev server cannot shut down the HMR connection cleanly.

Source

Thrown at packages/assets/src/lib/asset-server.ts:1127

    if (isClosed()) {
      channel.close()
      return null
    }

    setUnsubscribe(channel.onFileEvents(handleFileEvents))
    return channel
  })
}

function validateBrowserHmrChannel(channel: unknown): asserts channel is BrowserHmrChannel {
  if (channel === null || typeof channel !== 'object') {
    throw new TypeError('hmr must create an object')
  }
  if (!('url' in channel) || typeof channel.url !== 'string') {
    throw new TypeError('hmr must create a channel with a string url')
  }
  if (!('close' in channel) || typeof channel.close !== 'function') {
    throw new TypeError('hmr must create a channel with a close function')
  }
  if (!('onFileEvents' in channel) || typeof channel.onFileEvents !== 'function') {
    throw new TypeError('hmr must create a channel with an onFileEvents function')
  }
  if (!('updateWatchedFiles' in channel) || typeof channel.updateWatchedFiles !== 'function') {
    throw new TypeError('hmr must create a channel with an updateWatchedFiles function')
  }
}

function normalizeBasePath(basePath: string): string {
  if (typeof basePath !== 'string') {
    throw new TypeError('basePath must be a string')
  }

  return normalizePathname(basePath || '/').replace(/\/+$/, '') || '/'
}

function normalizeFingerprintOptions(options: {

View on GitHub (pinned to 9696913134)

Solutions

  1. Add a `close(): void` (or `close(): Promise<void>`) method to the object returned by `createChannel` that tears down the channel (e.g. closes the WebSocket/event stream)
  2. Verify the channel also implements `url: string`, `onFileEvents(fn)`, and `updateWatchedFiles(files)` or you will hit the adjacent errors next
  3. If you don't need a custom channel, remove the `hmr.createChannel` option to use the built-in channel

Example fix

// before
createChannel: () => ({ url, onFileEvents, updateWatchedFiles })
// after
createChannel: () => ({
  url,
  onFileEvents,
  updateWatchedFiles,
  close() {
    watcher.close()
  },
})
Defensive patterns

Strategy: validation

Validate before calling

let channel = createChannel()
if (typeof channel?.close !== 'function') throw new Error('custom HMR channel must implement close()')

Type guard

function isValidHmrChannel(c: unknown): c is { url: string; close: () => void; onFileEvents: (cb: (e: unknown[]) => void) => void; updateWatchedFiles: (f: string[]) => void } {
  return !!c && typeof c === 'object' &&
    typeof (c as any).url === 'string' &&
    typeof (c as any).close === 'function' &&
    typeof (c as any).onFileEvents === 'function' &&
    typeof (c as any).updateWatchedFiles === 'function'
}

Prevention

When it happens

Trigger: Passing `hmr: { createChannel: () => channel }` to the asset server where the returned object lacks a `close()` method (or `close` is not a function, e.g. a boolean or undefined).

Common situations: Custom HMR channel implementations that were written against an older Remix version with a smaller channel interface, or a partially-implemented mock channel in tests/plugins.

Related errors


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