vercel/ai · error
${method}: UI stream is already closed.
Error message
${method}: UI stream is already closed. What it means
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.
Source
Thrown at packages/rsc/src/streamable-ui/create-streamable-ui.tsx:62
* Once called, the UI node cannot be updated or appended anymore.
*
* This method is always **required** to be called, otherwise the response will be stuck in a loading state.
*/
done(...args: [React.ReactNode] | []): StreamableUIWrapper;
};
/**
* Create a piece of changeable UI that can be streamed to the client.
* On the client side, it can be rendered as a normal React node.
*/
function createStreamableUI(initialValue?: React.ReactNode) {
let currentValue = initialValue;
let closed = false;
let { row, resolve, reject } = createSuspendedChunk(initialValue);
function assertStream(method: string) {
if (closed) {
throw new Error(method + ': UI stream is already closed.');
}
}
let warningTimeout: NodeJS.Timeout | undefined;
function warnUnclosedStream() {
if (process.env.NODE_ENV === 'development') {
if (warningTimeout) {
clearTimeout(warningTimeout);
}
warningTimeout = setTimeout(() => {
console.warn(
'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.',
);
}, HANGING_STREAM_WARNING_TIME_MS);
}
}
warnUnclosedStream();
View on GitHub (pinned to 69428b1f8b)
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
Example fix
// before stream.done(); stream.update(<NewUI />); // throws // after stream.update(<NewUI />); stream.done();
Defensive patterns
Strategy: try-catch
Validate before calling
function assertOpen(stream: { closed?: boolean }) {
if (stream.closed) throw new Error('stream already closed');
} Try / catch
try {
stream.update(newUI);
} catch (e) {
if (e instanceof Error && e.message.endsWith('UI stream is already closed.')) {
return; // discard update after close
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ${method}: Value stream is already closed.
- ${method}: Value stream is locked and cannot be updated.
- `getMutableAIState` must be called before returning from an
- Invalid argument for parameter requests: requests must not b
- Invalid argument for parameter requests: request IDs must no
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/6af99a42d44fcf54.
Report an issue: GitHub.