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
- 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)
- Verify the channel also implements `url: string`, `onFileEvents(fn)`, and `updateWatchedFiles(files)` or you will hit the adjacent errors next
- 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
- Write a unit test that calls your createChannel and asserts all four required members
- Type the return of createChannel against the HmrChannel interface so the compiler flags missing members
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
- hmr must create a channel with an onFileEvents function
- hmr must create a channel with an updateWatchedFiles functio
- basePath must be a string
- fingerprint.buildId must be a string
- Browser HMR is disabled for this node-hmr runtime
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/5b792c3f669bd626.
Report an issue: GitHub.