Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/serverfn-dont-log-aborts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/start-client-core': patch
---

fix(start): don't log an aborted server function request as an error

`serverFnFetcher` logged every non-`Response` error before rethrowing, including the `AbortError` thrown when the caller cancels the request (e.g. TanStack Query aborting a query when its component unmounts). Cancellation is expected control flow, so it is no longer logged. Genuine errors are still logged, and all errors are still rethrown so callers can handle them.
20 changes: 19 additions & 1 deletion packages/start-client-core/src/client-rpc/serverFnFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,20 @@ async function getFetchBody(
return undefined
}

/**
* Whether an error is an abort, i.e. the caller cancelled the request (e.g. TanStack Query
* cancelling a query on unmount). Covers both `DOMException` (the standard `fetch` abort) and any
* `Error` with `name === 'AbortError'`.
*/
function isAbortError(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
(error as { name: unknown }).name === 'AbortError'
)
}

/**
* Retrieves a response from a given function and manages potential errors
* and special response types including redirects and not found errors.
Expand All @@ -238,7 +252,11 @@ async function getResponse(fn: () => Promise<Response>) {
if (error instanceof Response) {
response = error
} else {
console.log(error)
// A caller aborting the request is expected control flow, not a failure, so don't log it.
// We still rethrow so the caller can handle the cancellation.
if (!isAbortError(error)) {

@Sheraff Sheraff Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the only test we need is this

Suggested change
if (!isAbortError(error)) {
if (!(error instanceof Error) || error.name !== 'AbortError') {

console.log(error)
}
throw error
}
}
Expand Down
33 changes: 33 additions & 0 deletions packages/start-client-core/tests/serverFnFetcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { serverFnFetcher } from '../src/client-rpc/serverFnFetcher'

describe('serverFnFetcher error handling', () => {
afterEach(() => {
vi.restoreAllMocks()
})

it('does not log when the request is aborted, but still rethrows', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const abortError = new DOMException(
'The operation was aborted',
'AbortError',
)
const handler = vi.fn((): Promise<Response> => Promise.reject(abortError))

await expect(serverFnFetcher('/_serverFn/test', [{}], handler)).rejects.toBe(
abortError,
)
expect(logSpy).not.toHaveBeenCalled()
})

it('logs and rethrows a genuine error', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const error = new Error('boom')
const handler = vi.fn((): Promise<Response> => Promise.reject(error))

await expect(serverFnFetcher('/_serverFn/test', [{}], handler)).rejects.toBe(
error,
)
expect(logSpy).toHaveBeenCalledWith(error)
})
})