remix-run/remix · error · TypeError

hmr must create a channel with an onFileEvents function

Error message

hmr must create a channel with an onFileEvents function

What it means

The HMR channel returned by `hmr.createChannel` must expose an `onFileEvents` function so the asset server can subscribe to file change events for hot updates. This error fires when the channel object is missing that method or it is not callable.

Source

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

    }

    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: {
  fingerprint: AssetServerOptions['fingerprint']
  watch: AssetServerOptions['watch']
}):

View on GitHub (pinned to 9696913134)

Solutions

  1. Implement `onFileEvents(callback: (events: FileEvent[]) => void)` on the returned channel, storing the callback and invoking it when files change
  2. Confirm the other required members (`url`, `close`, `updateWatchedFiles`) exist too
  3. Drop the custom `createChannel` if the default HMR channel suffices

Example fix

// before
createChannel: () => ({ url, close, updateWatchedFiles })
// after
createChannel: () => ({
  url,
  close,
  updateWatchedFiles,
  onFileEvents(callback) {
    watcher.on('change', (file) => callback([{ type: 'update', file }]))
  },
})
Defensive patterns

Strategy: validation

Validate before calling

let channel = createChannel()
if (typeof channel.onFileEvents !== 'function') throw new Error('channel must implement onFileEvents')

Type guard

function hasOnFileEvents(c: unknown): c is { onFileEvents: (cb: (events: unknown[]) => void) => void } {
  return !!c && typeof (c as any).onFileEvents === 'function'
}

Prevention

When it happens

Trigger: `createChannel` returns an object without an `onFileEvents(callback)` method, so the asset server cannot register its file-event listener.

Common situations: Custom channel wrappers (e.g. around a bundler watcher or SSE stream) that only implement `url`/`close`; channels ported from older Remix versions; incomplete test doubles.

Related errors


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