{"record":{"id":"5b792c3f669bd626","repo":"remix-run/remix","slug":"hmr-must-create-a-channel-with-a-close-function","errorCode":null,"errorMessage":"hmr must create a channel with a close function","messagePattern":"hmr must create a channel with a close function","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/assets/src/lib/asset-server.ts","lineNumber":1127,"sourceCode":"    if (isClosed()) {\n      channel.close()\n      return null\n    }\n\n    setUnsubscribe(channel.onFileEvents(handleFileEvents))\n    return channel\n  })\n}\n\nfunction validateBrowserHmrChannel(channel: unknown): asserts channel is BrowserHmrChannel {\n  if (channel === null || typeof channel !== 'object') {\n    throw new TypeError('hmr must create an object')\n  }\n  if (!('url' in channel) || typeof channel.url !== 'string') {\n    throw new TypeError('hmr must create a channel with a string url')\n  }\n  if (!('close' in channel) || typeof channel.close !== 'function') {\n    throw new TypeError('hmr must create a channel with a close function')\n  }\n  if (!('onFileEvents' in channel) || typeof channel.onFileEvents !== 'function') {\n    throw new TypeError('hmr must create a channel with an onFileEvents function')\n  }\n  if (!('updateWatchedFiles' in channel) || typeof channel.updateWatchedFiles !== 'function') {\n    throw new TypeError('hmr must create a channel with an updateWatchedFiles function')\n  }\n}\n\nfunction normalizeBasePath(basePath: string): string {\n  if (typeof basePath !== 'string') {\n    throw new TypeError('basePath must be a string')\n  }\n\n  return normalizePathname(basePath || '/').replace(/\\/+$/, '') || '/'\n}\n\nfunction normalizeFingerprintOptions(options: {","sourceCodeStart":1109,"sourceCodeEnd":1145,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/assets/src/lib/asset-server.ts#L1109-L1145","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\ncreateChannel: () => ({ url, onFileEvents, updateWatchedFiles })\n// after\ncreateChannel: () => ({\n  url,\n  onFileEvents,\n  updateWatchedFiles,\n  close() {\n    watcher.close()\n  },\n})","handlingStrategy":"validation","validationCode":"let channel = createChannel()\nif (typeof channel?.close !== 'function') throw new Error('custom HMR channel must implement close()')","typeGuard":"function isValidHmrChannel(c: unknown): c is { url: string; close: () => void; onFileEvents: (cb: (e: unknown[]) => void) => void; updateWatchedFiles: (f: string[]) => void } {\n  return !!c && typeof c === 'object' &&\n    typeof (c as any).url === 'string' &&\n    typeof (c as any).close === 'function' &&\n    typeof (c as any).onFileEvents === 'function' &&\n    typeof (c as any).updateWatchedFiles === 'function'\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["hmr","dev-server","asset-server","validation"],"backgroundTag":"interface-contract-validation","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}