vercel/ai · error · UIMessageStreamError
Received text-end for missing text part with ID "${chunk.id}
Error message
Received text-end for missing text part with ID "${chunk.id}". Ensure a "text-start" chunk is sent before any "text-end" chunks. What it means
The stream processor looks up `state.activeTextParts[chunk.id]` when handling 'text-end'. If the text part was never started (or already ended), no active part exists and a UIMessageStreamError is thrown. Closing a text part requires a matching open text-start.
Source
Thrown at packages/ai/src/ui/process-ui-message-stream.ts:461
throw new UIMessageStreamError({
chunkType: 'text-delta',
chunkId: chunk.id,
message:
`Received text-delta for missing text part with ID "${chunk.id}". ` +
`Ensure a "text-start" chunk is sent before any "text-delta" chunks.`,
});
}
textPart.text += chunk.delta;
textPart.providerMetadata =
chunk.providerMetadata ?? textPart.providerMetadata;
write();
break;
}
case 'text-end': {
const textPart = state.activeTextParts[chunk.id];
if (textPart == null) {
throw new UIMessageStreamError({
chunkType: 'text-end',
chunkId: chunk.id,
message:
`Received text-end for missing text part with ID "${chunk.id}". ` +
`Ensure a "text-start" chunk is sent before any "text-end" chunks.`,
});
}
textPart.state = 'done';
textPart.providerMetadata =
chunk.providerMetadata ?? textPart.providerMetadata;
delete state.activeTextParts[chunk.id];
write();
break;
}
case 'custom': {
const customPart: CustomContentUIPart = {
type: 'custom',View on GitHub (pinned to 69428b1f8b)
Solutions
- Send exactly one 'text-start' with the same id before 'text-end', and emit 'text-end' only once per id.
- Verify ids are generated/stable consistently across start/delta/end chunks.
- Guard server code against emitting end events after the part was closed (track open part ids server-side).
- Catch UIMessageStreamError for text-end chunks and ignore already-closed parts.
Example fix
// before
writer.write({ type: 'text-end', id: 't1' }); // no matching open text-start
// after
writer.write({ type: 'text-start', id: 't1' });
writer.write({ type: 'text-delta', id: 't1', delta: 'Hi' });
writer.write({ type: 'text-end', id: 't1' }); Defensive patterns
Strategy: validation
Validate before calling
const openIds = new Set<string>();
function assertTextOpenBeforeEnd(id: string) {
if (!openIds.has(id)) throw new Error(`text-end without open text-start for id ${id}`);
openIds.delete(id); // prevents duplicate ends
} Try / catch
try {
await processUIMessageStream(...);
} catch (e) {
if (e instanceof Error && /text-end for missing text part/.test(e.message)) {
console.warn('Malformed stream: orphan text-end', e.message);
} else throw e;
} Prevention
- Emit text-end exactly once per text part and only after text-start.
- Generate start/end ids from the same variable to avoid mismatches.
- Add server-side lifecycle assertions/unit tests for chunk ordering.
- Avoid re-emitting end chunks on retry/resume paths.
When it happens
Trigger: A 'text-end' chunk with an id that never had a 'text-start', or a second 'text-end' for an already-closed text part, in a custom or replayed UI message stream.
Common situations: Duplicate text-end chunks from buggy server code; id mismatches between start and end; resuming streams where the text part was already finalized; hand-rolled SSE emitters out of order.
Related errors
- Received text-delta for missing text part with ID "${chunk.i
- Received reasoning-delta for missing reasoning part with ID
- Received reasoning-end for missing reasoning part with ID "$
- Received tool-input-delta for missing tool call with ID "${c
- No tool invocation found for tool call ID "${toolCallId}".
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/0a094d4f6aa73210.
Report an issue: GitHub.