The AI SDK, by default, does not always wait for a stream to be completed. This is surprising - and can trip you up when you're relying on the onFinish callback to be called.
In this code here, we're calling streamText, passing "Hello, world!" to Gemini 2.0 Flash, and we have an onFinish on the streamTextResult.
import { google } from '@ai-sdk/google';import { streamText } from 'ai';console.log('Process starting...');const streamTextResult = streamText({model: google('gemini-2.0-flash'),prompt: 'Hello, world!',
Our expectation here is that the stream is going to complete its work, and then it's going to say "Stream finished", and log that to the terminal.
In theory, we should get three logs:
However, when we run this, we actually don't get that. We get:
So onFinish here is never actually called. "Stream finished!" never actually appears.
That's because even though we're getting streaming data coming from the LLM, we're not actually processing the parts of that stream.
If we do process them, for instance, using a for-await loop, our code would look like this:
import { google } from '@ai-sdk/google';import { streamText } from 'ai';console.log('Process starting...');const streamTextResult = streamText({model: google('gemini-2.0-flash'),prompt: 'Hello, world!',
Then we'll see that we get:
So, processing the stream's parts is a way to make sure that the stream finishes.
However, there are some situations where we want to consume the entire stream and make sure that the stream finishes without necessarily processing all of the parts. Or we might want to await the result of a stream without necessarily processing all the bits as well.
For that, we can use the consumeStream() method on the streamTextResult as shown in explainer.1:
import { google } from '@ai-sdk/google';import { streamText } from 'ai';console.log('Process starting...');const streamTextResult = streamText({model: google('gemini-2.0-flash'),prompt: 'Hello, world!',
This will wait for the stream to complete and consume all of the parts, and then trigger the onFinish.
The most common use case for this is when you have persistence logic inside your onFinish, because if there's a network connection issue, then your stream will interrupt, and your stream won't be fully consumed, so your onFinish won't be hit.
If we run this with consumeStream(), we'll see that we get:
as we expect.
consumeStream FunctionNow this is not just available on the return type of streamTextResult too. There's also a top level function called consumeStream, which can consume a readable stream until it's fully read, as shown in explainer.2:
We're calling consumeStream with a streamTextResult and creating a UI message stream out of the streamTextResult. So functionally, this is doing the same thing as before. We're just doing an extra step where we turn it into a UI message stream instead.
Let's run it and see if it works, and we can see we have:
So if you have a situation where your onFinish callbacks are not being called, it's likely that some variety of consumeStream will help make sure that your streams finish.
I recommend checking out these two explainers, running them a few times, trying commenting them in and out, trying to break it if you can.
Good luck and I will see you in the next one.
import { google } from '@ai-sdk/google';import { consumeStream, streamText } from 'ai';console.log('Process starting...');const streamTextResult = streamText({model: google('gemini-2.0-flash'),prompt: 'Hello, world!',onFinish: () => {console.log('Stream finished!');},});// Using the top-level consumeStream functionawait consumeStream({stream: streamTextResult.toUIMessageStream(),});console.log('Process exiting...');