vercel/ai · error · Error

${method}: Value stream is already closed.

Error message

${method}: Value stream is already closed.

What it means

createStreamableValue's assertStream guard throws this when any mutating method (update, append, error, done) is called after the stream has been closed via .close(). Closed streams can no longer emit values to the client.

Source

Thrown at packages/rsc/src/streamable-value/create-streamable-value.ts:137

   * updated by the user.
   */
  [STREAMABLE_VALUE_INTERNAL_LOCK]: boolean;
};

function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
  let closed = false;
  let locked = false;
  let resolvable = createResolvablePromise<StreamableValue<T, E>>();

  let currentValue = initialValue;
  let currentError: E | undefined;
  let currentPromise: typeof resolvable.promise | undefined =
    resolvable.promise;
  let currentPatchValue: StreamablePatch;

  function assertStream(method: string) {
    if (closed) {
      throw new Error(method + ': Value stream is already closed.');
    }
    if (locked) {
      throw new Error(
        method + ': Value stream is locked and cannot be updated.',
      );
    }
  }

  let warningTimeout: NodeJS.Timeout | undefined;
  function warnUnclosedStream() {
    if (process.env.NODE_ENV === 'development') {
      if (warningTimeout) {
        clearTimeout(warningTimeout);
      }
      warningTimeout = setTimeout(() => {
        console.warn(
          'The streamable value has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.',
        );

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Track stream lifecycle; stop emitting after close() is called
  2. Call done() or close() only once, at the very end of producing values
  3. Restructure so all emissions happen before closing (e.g. await all appends before done())
  4. Wrap emissions in a guard checking your own closed flag

Example fix

// before
streamable.update(next);
streamable.close();
streamable.update(more); // throws
// after
streamable.update(next);
streamable.update(more);
streamable.close();
Defensive patterns

Strategy: validation

Validate before calling

let isClosed = false;
function safeUpdate(streamable, value) {
  if (isClosed) return; // skip emissions after close
  streamable.update(value);
}
// set isClosed = true wherever you call streamable.close()

Type guard

function canEmit(streamable) {
  return streamable && !streamable.closed; // track your own closed flag if not exposed
}

Try / catch

try {
  streamable.update(value);
} catch (e) {
  if (!/Value stream is already closed/.test(String(e.message))) throw e;
  // else: emission after close — safe to ignore or log
}

Prevention

When it happens

Trigger: Calling .update()/.append()/.error()/.done() on a streamable value after .close() was already invoked — often from code that continues running after a server action finishes or a duplicate code path closes the stream.

Common situations: Background tasks or event handlers that keep emitting after close(); calling done() then append(); React Server Component streams finalized before an async continuation runs.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/d8a2a9ec882b2d33. Report an issue: GitHub.