Describe the bug
It's not clear what the interaction between useLiveSuspenseQuery and ErrorBoundary is supposed to look like. Currently, if the fetch function throws, it isnt caught by the ErrorBoundary
To Reproduce
Steps to reproduce the behavior:
https://stackblitz.com/edit/vitejs-vite-w4tkecqk?file=src%2FApp.tsx
Copy pasted snippet of code
import React, { Suspense, useState } from 'react';
import { createCollection, useLiveSuspenseQuery } from '@tanstack/react-db';
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { QueryClient } from '@tanstack/query-core';
import { ErrorBoundary } from 'react-error-boundary';
interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: number;
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
let mockDb: Todo[] = [
{
id: '1',
text: 'Learn TanStack DB',
completed: false,
createdAt: Date.now(),
},
];
const api = {
getTodos: async (): Promise<Todo[]> => {
await sleep(1000);
throw new Error('random error');
// Uncomment this for normal usage.
// return [...mockDb];
},
addTodo: async (todo: Todo): Promise<Todo> => {
await sleep(600);
mockDb.push(todo);
return todo;
},
updateTodo: async (id: string, updates: Partial<Todo>): Promise<Todo> => {
await sleep(800);
const index = mockDb.findIndex((t) => t.id === id);
mockDb[index] = { ...mockDb[index], ...updates };
return mockDb[index];
},
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
throwOnError: true,
},
},
});
const todoCollection = createCollection(
queryCollectionOptions({
queryClient: queryClient,
queryKey: ['todos'],
queryFn: () => api.getTodos(),
getKey: (todo: Todo) => todo.id,
onUpdate: async ({ transaction }) => {
// The transaction contains the modified data
const { modified } = transaction.mutations[0];
await api.updateTodo(modified.id, modified);
},
onInsert: async ({ transaction }) => {
// The transaction contains the modified data
const { modified } = transaction.mutations[0];
await api.addTodo(modified);
},
})
);
function TodoList() {
const [text, setText] = useState('');
const { data: todos = [] } = useLiveSuspenseQuery((q) =>
q
.from({ todo: todoCollection })
.orderBy(({ todo }) => todo.createdAt, 'desc')
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text.trim()) return;
const newTodo: Todo = {
id: Math.random().toString(36).slice(2, 9),
text,
completed: false,
createdAt: Date.now(),
};
// collection.insert updates the UI state instantly
todoCollection.insert(newTodo);
setText('');
};
const toggleTodo = (todo: Todo) => {
todoCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed;
});
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map((todo) => (
<li
key={todo.id}
onClick={() => toggleTodo(todo)}
style={{
cursor: 'pointer',
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text} {todo.completed ? '✅' : '⭕'}
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<div>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<p>Loading...</p>}>
<TodoList />
</Suspense>
</ErrorBoundary>
</div>
);
}
package.json
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/query-db-collection": "^1.0.28",
"@tanstack/react-db": "^0.1.75",
"@tanstack/react-query": "^5.90.21",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-error-boundary": "^6.1.1"
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@types/node": "^24.11.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.3",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1"
}
}
tl;dr
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<p>Loading...</p>}>
<TodoList />
</Suspense>
</ErrorBoundary>
// in TodoList
const { data: todos = [] } = useLiveSuspenseQuery((q) =>
q
.from({ todo: todoCollection })
.orderBy(({ todo }) => todo.createdAt, 'desc')
);
// in collection
const todoCollection = createCollection(
queryCollectionOptions({
queryClient: queryClient,
queryKey: ['todos'],
queryFn: () => api.getTodos(),
// api.getTodos()
const api = {
getTodos: async (): Promise<Todo[]> => {
await sleep(1000);
throw new Error('random error');
// Uncomment this for normal usage.
// return [...mockDb];
},
Expected behavior
I expected the error to be caught by the error boundary.
Workaround
Throwing the collection error if encountered:
const { data:todos } = useLiveSuspenseQuery((q) => q.from({ todo: todoCollection }))
if (todoCollection.utils.isError) {
throw todoCollection.utils.lastError
}
Describe the bug
It's not clear what the interaction between
useLiveSuspenseQueryandErrorBoundaryis supposed to look like. Currently, if the fetch function throws, it isnt caught by theErrorBoundaryTo Reproduce
Steps to reproduce the behavior:
https://stackblitz.com/edit/vitejs-vite-w4tkecqk?file=src%2FApp.tsx
Copy pasted snippet of code
package.json
{ "name": "vite-react-typescript-starter", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@tanstack/query-db-collection": "^1.0.28", "@tanstack/react-db": "^0.1.75", "@tanstack/react-query": "^5.90.21", "react": "^19.2.4", "react-dom": "^19.2.4", "react-error-boundary": "^6.1.1" }, "devDependencies": { "@eslint/js": "^9.39.3", "@types/node": "^24.11.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", "eslint": "^9.39.3", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.3.1" } }tl;dr
Expected behavior
I expected the error to be caught by the error boundary.
Workaround
Throwing the collection error if encountered: