1. Topics
  2. /
  3. AI Stack
  4. /
  5. Why Vercel AI SDK stream errors fail silently in production
11 min readPublished Jul 8, 2026

Why Vercel AI SDK stream errors fail silently in production

You wrapped your route handler in try/catch. Every request logged as a 200. Then something broke mid-stream – a truncated response, or nothing at all. No thrown error. No log entry. No status code. The three places you'd normally check for a failure – try/catch, request logs, and the HTTP status code – can't carry an error once the stream has started.

streamText is the Vercel AI SDK function that generates LLM output as a stream. Instead of waiting for the full response, the server pushes tokens to the client as the model produces them, over a single HTTP response that stays open for the duration of generation. That open connection is where every failure in this piece happens – and why none of your usual error-handling tools can see it.

Copy link to clipboard

Key takeaways

  • streamText sends the HTTP 200 and opens the response body before the LLM produces a single token. Once that commitment is made, the HTTP status code is no longer available as an error channel — so checking res.status won't tell you anything went wrong mid-generation.

  • Wrapping your route handler in try/catch will not catch mid-stream errors. Those errors occur after the HTTP response has opened, in a context try/catch cannot reach.

  • Use onError for most generation errors – it's what Vercel's own troubleshooting guide (ai-sdk.dev/docs/troubleshooting/stream-text-not-working) recommends. Iterate over fullStream when you need to handle tool errors separately. If both come up empty, the failure is likely at the transport layer, not the SDK.

  • Surfacing transport-layer failures – disconnects, crashed server processes, or network interruptions – requires either external monitoring or a transport layer that can distinguish a clean close from a crash. None of the patterns above catch them.

Copy link to clipboard

Why streamText opens the response stream before generation is complete

streamText opens the response and starts streaming tokens before generation finishes, trading a locked-in HTTP status for low first-token latency. That trade-off is also why errors that occur mid-stream can't signal failure through the status code.

HTTP commits its status code and headers before the body begins. A server that wants to return a 500 has to decide before writing the first byte. That constraint comes from HTTP itself, not from any choice the Vercel AI SDK makes.

streamText operates within that constraint. It calls toUIMessageStreamResponse(), which sets the status to 200 and opens the response stream immediately. The client starts receiving tokens as they arrive, instead of waiting for the full response.

That's the cost of low first-token latency: once the response opens, the server can't signal a problem via HTTP status codes. The 200 is immutable. Any error during generation has to travel a different route – through the data stream itself.

Copy link to clipboard

Why try/catch doesn't catch mid-stream errors

Wrapping a route handler's logic in try/catch is the standard way to catch failures in a Node.js request handler. It catches a bad request body, a failed database call, or a thrown exception anywhere in the synchronous or awaited code, which is why it's the first thing developers reach for here too. Its failure to catch anything mid-stream is disorienting precisely because nothing about the code looks wrong. You already know part of why: the HTTP 200 is committed before generation starts, so try/catch's job is done before any mid-stream error can occur. Two more things about how streamText works explain the rest.

Copy link to clipboard

Errors are embedded in the data stream

When generation fails mid-stream, the Vercel AI SDK doesn't throw a JavaScript exception. It writes a structured error part into the data stream instead – an event with type: 'error' or type: 'tool-error'. try/catch only watches for thrown exceptions. An event written into a stream is not a thrown exception, so try/catch never sees it.

Copy link to clipboard

toUIMessageStreamResponse() suppresses error messages by default

toUIMessageStreamResponse() suppresses the error message by default. The client sees that something went wrong, but not what – unless you configure a custom getErrorMessage function. This is deliberate: an exposed error could leak file paths, library internals, or database text into the browser.

How to surface mid-stream errors in Vercel AI SDK v6

try/catch, request logs, and HTTP status codes can't see mid-stream errors. The onError callback and fullStream iteration can, from inside the SDK. External observability tools add another layer for what those callbacks miss. Anything before the response opens still uses standard HTTP error handling.

Copy link to clipboard

1: The onError callback

The onError callback fires server-side when streamText encounters an error during generation. It receives the error object directly.

import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function POST(req) {
  const { messages } = await req.json();
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    onError: ({ error }) => {
      // Fires server-side after HTTP 200 is committed
      console.error('Generation error:', error);
    },
  });
  return result.toUIMessageStreamResponse();
}

This is the pattern Vercel's own troubleshooting guide recommends. It covers errors during generation but does not fire on stream aborts.

Copy link to clipboard

2: fullStream part iteration

fullStream is an async iterable that yields typed stream parts, including 'error', 'tool-error', and 'abort' events. Iterating it gives you per-type access to every event in the stream, which is useful when you need to handle tool errors separately from model errors, or when you want to inspect stream contents server-side.

import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function POST(req) {
  const { messages } = await req.json();
  const result = streamText({ model: openai('gpt-4o'), messages });

  // Consume the stream for logging concurrently with client delivery
  (async () => {
    for await (const part of result.fullStream) {
      if (part.type === 'error') console.error('Model error:', part.error);
      if (part.type === 'tool-error') console.error('Tool error:', part.error);
    }
  })().catch(console.error);

  return result.toUIMessageStreamResponse();
}

fullStream and toUIMessageStreamResponse() can run concurrently. The Vercel AI SDK manages internal buffering, but if you exhaust fullStream before returning the response, the client stream will stall.

Copy link to clipboard

Beyond the SDK: external observability instrumentation

Server-side observability platforms like Langfuse, LangSmith, or OpenTelemetry give you a monitoring layer outside the Vercel AI SDK's own callbacks. streamText has native OpenTelemetry support built in: setting experimental_telemetry: { isEnabled: true } traces the call without any additional wrapping, and platforms like Langfuse ingest those traces directly.

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  experimental_telemetry: { isEnabled: true },
});

These tools wrap the model call or intercept the stream at the infrastructure level, capturing failures that occur before onError fires and preserving the full generation trace for debugging.

This complements the SDK-native callbacks rather than replacing them. onError fires when the SDK intercepts a generation error. External instrumentation captures crashes, timeouts, and infrastructure failures at a layer above the SDK entirely.

Copy link to clipboard

What onFinish covers and what onAbort adds

The patterns above catch errors while generation is happening. A different question is how the stream ends at all – whether it completed, failed, or was cut short before the SDK or the model considered it done. That distinction matters because a client disconnect or a cancelled request isn't a generation error, so onError and fullStream won't report it. Two more callbacks cover the stream's terminal state directly: onFinish and onAbort.

onFinish fires after the stream completes with the full generation result: text, usage statistics, tool call results, and finish reason. It is the right place for post-generation logging, analytics writes, and database updates that depend on the complete output.

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  onFinish: ({ text, finishReason, usage }) => {
    // finishReason: 'stop', 'length', 'content-filter', 'tool-calls', 'error'
    if (finishReason === 'error') {
      console.error('Generation finished with error. Partial text:', text);
    }
    db.logCompletion({ text, finishReason, tokens: usage.totalTokens });
  },
});

onFinish does not fire on stream abort. If a client closes the connection mid-stream, or if you cancel via AbortSignal, onFinish will not execute. A stream abort is a different terminal state from a stream completion, and the SDK treats them separately.

Vercel added the onAbort callback specifically to fill this gap, giving you a fresh place to run abort cleanup: partial response logging, billing reconciliation, in-progress tool call teardown.

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  onFinish: ({ finishReason }) => {
    logCompletion(finishReason); // Fires: stop, length, error, content-filter
  },
  onAbort: () => {
    logAbort(); // Fires: client disconnect, AbortSignal
  },
});

The table below summarizes what each of the four callbacks catches and what it doesn't:

Callback

Fires when

What it gives you

What it doesn't cover

onError

A generation error occurs (rate limit, context length, provider error)

The error object, server-side

Stream aborts, tool-specific errors

fullStream

Any typed stream part is emitted

Per-type access to 'error', 'tool-error', and 'abort' events

Nothing is emitted if the connection itself fails before any part is written

onFinish

The stream completes, including with an error finish reason

Full generation result: text, usage, tool calls, finish reason

Aborted streams – does not fire on disconnect or cancellation

onAbort

The stream is cut short by AbortSignal or client disconnect

A place to run abort cleanup: partial logging, billing reconciliation, tool call teardown

Generation errors that complete normally instead of aborting

Together, onFinish and onAbort account for every way a generation can end. But both operate at the SDK layer, and neither can tell you what happened at the connection itself, one level below.

What HTTP's transport layer doesn't tell you

With direct HTTP streaming, a connection drop is ambiguous. The TCP connection closes, but there is no typed signal distinguishing "generation finished cleanly" from "server process crashed and the socket was forcibly closed." The client retries or fails depending on how it interprets an interrupted stream.

Four callbacks – onError, fullStream, onFinish, onAbort – give complete coverage of what the SDK itself did during generation. None of them can tell you what happened to the connection: whether the client received the stream before it disconnected, whether the server process crashed mid-generation, or whether the stream was terminated by infrastructure rather than the SDK. Neither can the user, who just sees a truncated response or a spinner that never resolves.

GitHub issue #4726 captured this before onError existed, describing the behavior as: "FAILS SILENTLY AND SWALLOWS ALL ERRORS!!" That frustration makes sense: someone debugging a production regression who expects HTTP semantics will look in the wrong places first. The SDK has since added onError as a first-class callback, but the underlying architecture hasn't changed.

Copy link to clipboard

What Ably AI Transport changes at the transport layer

Ably AI Transport replaces the ephemeral HTTP stream with a durable session that any connected client can subscribe to. At the transport layer, this changes two things directly relevant to the problems on this page.

The connection state is observable. With HTTP streaming, an unexpected server crash and a normal stream end look identical at the TCP layer.

A durable session exists independently of any single connection. Opting into Ably Presence alongside AI Transport requires the agent to enter presence at startup. In exchange, an unexpected crash surfaces as a presence departure rather than an ambiguous TCP close.

Cancellation is an explicit message, not a connection close. With HTTP streaming, the only upstream signal a client has is closing the connection, which the server cannot distinguish from a network failure. AI Transport cancellation is a typed message published on the durable session — see how AI Transport implements cancellation as a typed channel message, independent of connection state. The server receives it as a cancel event, fires the abortSignal, and every subscribed client sees the reason explicitly.

This is what Ably AI Transport adds at the transport layer. The generation-layer errors handled by onError, fullStream, onFinish, and onAbort are still your problem.

For the session architecture that Ably adds on top of the Vercel AI SDK, see the Ably AI Transport integration guide.

Where the gap actually is 

Silent stream failures aren't a Vercel AI SDK bug. They're what happens when HTTP's request-response model meets a connection that has to stay open for as long as generation takes. onError and fullStream cover generation-layer errors; onFinish and onAbort cover how the stream ends. None of the four can see the connection itself – that's the gap a durable transport layer like Ably AI Transport is built to close.

Copy link to clipboard

Frequently asked questions

Copy link to clipboard

How do I catch errors in a Vercel AI SDK stream?

Use the onError callback on streamText. It fires server-side when a generation error occurs, with the error object passed as an argument. For tool-specific errors or abort events, iterate result.fullStream and switch on part.type. Neither approach requires changes to your HTTP response handling.

Copy link to clipboard

Why does try/catch not work for Vercel AI SDK mid-stream errors?

A try/catch catches errors thrown during setup, before the response opens. Mid-stream errors occur after the handler has already written the HTTP 200 and begun streaming. Those errors are embedded in the stream protocol as typed events, not thrown exceptions. try/catch will not intercept them.

Copy link to clipboard

What does toUIMessageStreamResponse() do with errors by default?

It writes error events into the data stream but suppresses the error message from the client-visible stream. The client receives an error event type but the message content is empty. To expose error details to the client, configure a custom getErrorMessage function on toUIMessageStreamResponse.

Copy link to clipboard

Does onFinish fire when a stream is aborted?

No. onFinish fires when a stream completes, including with an error finish reason, but not when the stream is aborted via AbortSignal or client disconnect. Use the onAbort callback for abort handling. The two callbacks cover distinct terminal states: onFinish for completion, onAbort for cancellation.

Copy link to clipboard

How do I handle tool errors in Vercel AI SDK streams?

Tool errors appear as 'tool-error' parts in result.fullStream. Iterate fullStream server-side and check part.type === 'tool-error' to capture them separately from model errors. Tool errors can occur when a tool execution throws, when tool output fails to serialize, or when the model requests a tool not in the schema. The onError callback does not distinguish tool errors from model errors. fullStream does.

Research basis: GitHub issue #4726 (vercel/ai repository), now resolved in [email protected] via PR #4729 – the issue drove the addition of onError as a first-class API. Vercel AI SDK documentation and changelog. Ably AI Transport documentation. Vercel troubleshooting guide.

Join the Ably newsletter today

1000s of industry pioneers trust Ably for monthly insights on the realtime data economy.
Enter your email