{"record":{"id":"6af99a42d44fcf54","repo":"vercel/ai","slug":"method-ui-stream-is-already-closed","errorCode":null,"errorMessage":"${method}: UI stream is already closed.","messagePattern":"(.+?): UI stream is already closed\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/rsc/src/streamable-ui/create-streamable-ui.tsx","lineNumber":62,"sourceCode":"   * Once called, the UI node cannot be updated or appended anymore.\n   *\n   * This method is always **required** to be called, otherwise the response will be stuck in a loading state.\n   */\n  done(...args: [React.ReactNode] | []): StreamableUIWrapper;\n};\n\n/**\n * Create a piece of changeable UI that can be streamed to the client.\n * On the client side, it can be rendered as a normal React node.\n */\nfunction createStreamableUI(initialValue?: React.ReactNode) {\n  let currentValue = initialValue;\n  let closed = false;\n  let { row, resolve, reject } = createSuspendedChunk(initialValue);\n\n  function assertStream(method: string) {\n    if (closed) {\n      throw new Error(method + ': UI stream is already closed.');\n    }\n  }\n\n  let warningTimeout: NodeJS.Timeout | undefined;\n  function warnUnclosedStream() {\n    if (process.env.NODE_ENV === 'development') {\n      if (warningTimeout) {\n        clearTimeout(warningTimeout);\n      }\n      warningTimeout = setTimeout(() => {\n        console.warn(\n          'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.',\n        );\n      }, HANGING_STREAM_WARNING_TIME_MS);\n    }\n  }\n  warnUnclosedStream();\n","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/rsc/src/streamable-ui/create-streamable-ui.tsx#L44-L80","documentation":"createStreamableUI returns update/append/error/done mutators that may only be used while the UI stream is open. Once done/error (or the resolver completing the stream) closes it, any further mutation calls hit assertStream and throw, prefixed with the method name.","triggerScenarios":"Calling stream.update(...) or stream.append(...) after stream.done() or stream.error() has been called; calling mutators after the resolver returned/completed the stream; calling a mutator twice after close in async code paths.","commonSituations":"Firing stream.update from a background task or setTimeout that outlives the streamed response; duplicate done() calls across branches (both success and error paths); post-response cache-refresh code attempting to push UI updates.","solutions":["Guard mutations with a flag that is set when done/error is called","Ensure done()/error() is called exactly once and only after all update/append calls","Move long-running work so it completes before closing, or discard results after close instead of updating"],"exampleFix":"// before\nstream.done();\nstream.update(<NewUI />); // throws\n// after\nstream.update(<NewUI />);\nstream.done();","handlingStrategy":"try-catch","validationCode":"function assertOpen(stream: { closed?: boolean }) {\n  if (stream.closed) throw new Error('stream already closed');\n}","typeGuard":null,"tryCatchPattern":"try {\n  stream.update(newUI);\n} catch (e) {\n  if (e instanceof Error && e.message.endsWith('UI stream is already closed.')) {\n    return; // discard update after close\n  }\n  throw e;\n}","preventionTips":["Call done()/error() exactly once, as the final action","Track stream open state in a boolean before mutating","Avoid updating streams from timers/promises that may outlive the response"],"tags":["streaming","lifecycle","react-server-components"],"backgroundTag":"stream-already-closed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}