{"record":{"id":"0a094d4f6aa73210","repo":"vercel/ai","slug":"received-text-end-for-missing-text-part-with-id","errorCode":null,"errorMessage":"Received text-end for missing text part with ID \"${chunk.id}\". Ensure a \"text-start\" chunk is sent before any \"text-end\" chunks.","messagePattern":"Received text-end for missing text part with ID \"(.+?)\"\\. Ensure a \"text-start\" chunk is sent before any \"text-end\" chunks\\.","errorType":"exception","errorClass":"UIMessageStreamError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/ui/process-ui-message-stream.ts","lineNumber":461,"sourceCode":"                throw new UIMessageStreamError({\n                  chunkType: 'text-delta',\n                  chunkId: chunk.id,\n                  message:\n                    `Received text-delta for missing text part with ID \"${chunk.id}\". ` +\n                    `Ensure a \"text-start\" chunk is sent before any \"text-delta\" chunks.`,\n                });\n              }\n              textPart.text += chunk.delta;\n              textPart.providerMetadata =\n                chunk.providerMetadata ?? textPart.providerMetadata;\n              write();\n              break;\n            }\n\n            case 'text-end': {\n              const textPart = state.activeTextParts[chunk.id];\n              if (textPart == null) {\n                throw new UIMessageStreamError({\n                  chunkType: 'text-end',\n                  chunkId: chunk.id,\n                  message:\n                    `Received text-end for missing text part with ID \"${chunk.id}\". ` +\n                    `Ensure a \"text-start\" chunk is sent before any \"text-end\" chunks.`,\n                });\n              }\n              textPart.state = 'done';\n              textPart.providerMetadata =\n                chunk.providerMetadata ?? textPart.providerMetadata;\n              delete state.activeTextParts[chunk.id];\n              write();\n              break;\n            }\n\n            case 'custom': {\n              const customPart: CustomContentUIPart = {\n                type: 'custom',","sourceCodeStart":443,"sourceCodeEnd":479,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/ai/src/ui/process-ui-message-stream.ts#L443-L479","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nwriter.write({ type: 'text-end', id: 't1' }); // no matching open text-start\n// after\nwriter.write({ type: 'text-start', id: 't1' });\nwriter.write({ type: 'text-delta', id: 't1', delta: 'Hi' });\nwriter.write({ type: 'text-end', id: 't1' });","handlingStrategy":"validation","validationCode":"const openIds = new Set<string>();\nfunction assertTextOpenBeforeEnd(id: string) {\n  if (!openIds.has(id)) throw new Error(`text-end without open text-start for id ${id}`);\n  openIds.delete(id); // prevents duplicate ends\n}","typeGuard":null,"tryCatchPattern":"try {\n  await processUIMessageStream(...);\n} catch (e) {\n  if (e instanceof Error && /text-end for missing text part/.test(e.message)) {\n    console.warn('Malformed stream: orphan text-end', e.message);\n  } else throw e;\n}","preventionTips":["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."],"tags":["streaming","ui-message-stream","protocol-violation","text-parts"],"backgroundTag":"missing-stream-chunk","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}