TanStack / TanStack/ai

Hydration failure with persistence: true is silent, apps can't show a load error

Open
#1,331 0 comments 0 reactions 1 assignee View on GitHub

@jherr is already working on this.

Since Sep 17, 2026.

has-pr waiting-on: maintainer
Dominant language
TypeScript
Stars
3.1k
Forks
331
Avg merge
1d 22h
Merged PRs (30d)
160

Description

TanStack AI version

@tanstack/ai-react v0.23.0 (@tanstack/ai-client v0.30.0; verified unchanged in v0.31.0 and main)

Framework/Library version

React v19

Describe the bug and the steps to reproduce it

What happens

I'm using persistence: true, so the client loads an existing thread from my server when the chat mounts. When that load fails (my server returned a 500), the failure is invisible: onError doesn't fire, chat.error stays undefined, and the client reports status: 'ready' with zero messages, exactly what it reports for a thread that's genuinely empty. So my app can't tell "this thread failed to load" from "this thread has no messages", and the user is stuck on an empty/loading screen. There's no retry either; the load happens once on mount.

Steps to reproduce

No provider/API key needed, the failure happens before any send:

const chat = useChat({
	connection: fetchServerSentEvents('/nonexistent-endpoint'), // hydrate GET fails
	persistence: true,
	threadId: 'thread-1',
	onError: (e) => console.log('onError', e), // never logs
})
// Render chat.status + chat.error: status stays 'ready', error stays undefined,
// while the Network tab shows the failed GET.

What I expected

An error signal. GenerationClient.failHydration handles this exact case (status: 'error' + onError) with the comment "so a broken server is distinguishable from an empty one and the app can retry."

Cause

hydrateFromServer in chat-client.ts:

try {
	result = await hydrate(this.threadId)
} catch {
	return
}

No callback, state change, event, or retry is reachable from that catch. Same in 0.30.0, 0.31.0, and main.

Related breakage

For apps where users supply their own provider key (BYOK), a 401 during hydrate throws ByokMissingError into this same catch, so the key-request flow never triggers on thread load. The send path special-cases that error; the hydrate path eats it.

Fix

Minimal: catch (error) { this.callbacksRef.current.onError(error) }.
Fuller: port the failHydration pattern so state reflects it too.
(#1120 asks for hydration state generally, this may want to fold into that.)

Happy to PR either. Which shape do you prefer?

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

https://codesandbox.io/p/sandbox/pvgmmh?file=%2Findex.js

Screenshots or Videos (Optional)

No response

Do you intend to try to help solve this bug with your own PR?

Yes, I think I know how to fix it and will discuss it in the comments of this issue

Terms & Code of Conduct
  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.