Which project does this relate to?
Start
Describe the bug
If you pass a function call that the compiler is supposed to transform directly into the argument of createServerFn().handler(), the build will succeed, but it will generate a broken server function.
createServerOnlyFn
TypeError: Cannot create property 'method' on string 'da38dc3f0228fb3e…'
at fun.handler (dist/server/server.js:237:23)
at loadEntries (dist/server/server.js:1419:52)
As a result, it will return 500.
Since it is thrown during module evaluation inside loadEntries, it fails before routing, causing not only that route but the entire application to crash.
createIsomorphicFn
// Broken
.handler(createServerOnlyFn(impl))
.handler(createIsomorphicFn().server(impl).client(other))
// Works (same JavaScript)
const wrapped = createServerOnlyFn(impl)
.handler(wrapped)
No exception is thrown, but the server function returns undefined.
If you bind the call to a variable first and pass the identifier, both will be generated correctly.
Generated Code
Caller chunk (dist/server/assets/router-*.js):
// hoisted → Correct
createServerFn({ method: "GET" }).handler(createSsrRpc("da38dc3f0228fb3e…"))
// inline-server-only → ID string remains
createServerFn({ method: "GET" }).handler("da38dc3f0228fb3e…")
// inline-isomorphic → Implementation overwrites the stub
createServerFn({ method: "GET" }).handler(readOnServer)
The client bundle has the same broken slot, and in both cases, a local function is included instead of an RPC.
// inline-server-only
.handler(() => { throw Error("createServerOnlyFn() functions can only be called on the server!") })
// inline-isomorphic
.handler(() => ({ ranOn: "client" }))
Cause
Candidates are collected in a single pass, and each holds a Babel path. The handlers then run in a fixed order defined by BuiltInKindHandlerOrder in packages/start-plugin-core/src/start-compiler/compiler.ts:
['ServerFn', 'Middleware', 'IsomorphicFn', 'ServerOnlyFn', 'ClientOnlyFn']
ServerFn runs first, and handleCreateServerFn.ts:445 replaces the first argument of .handler().
handlerFnPath.replaceWith(rpcStub) // rpcStub is createSsrRpc("<id>")
Subsequent handlers operate through the paths captured before this replacement. From there, they diverge into two cases:
handleEnvOnly.ts reads path.node.arguments[0]. Since the node pointed to by the path is now createSsrRpc("<id>"), what it retrieves is the string literal of the ID. This leaves .handler("<id>"), and the handler of createServerFn attempts to assign extractedFn.method to that string.
handleCreateIsomorphicFn.ts retains envCallInfo.firstArgPath?.node captured from the original tree, so path.replaceWith(innerFn) overwrites the stub with the implementation itself. The RPC wiring disappears, and no exception is thrown.
The root cause is the same in both cases: subsequent handlers are touching the node replaced by ServerFn through stale paths.
Binding it to a variable avoids this because the node for createServerOnlyFn(...) moves to the initializer of the variable declaration, and none of the handlers replace it there.
Complete minimal reproducer
https://github.com/imaimai17468/tanstack-start-handler-call-expression-repro
Steps to Reproduce the Bug
git clone https://github.com/imaimai17468/tanstack-start-handler-call-expression-repro
cd tanstack-start-handler-call-expression-repro
npm install
npm run repro
This builds three apps, displays the generated .handler(...) in dist/server, and starts vite preview so you can make a request to /.
The results are as follows:
| app |
Shape of .handler() |
GET / |
apps/hoisted |
Passes const wrapped = createServerOnlyFn(impl) |
200, {"ranOn":"server"} |
apps/inline-server-only |
Writes createServerOnlyFn(impl) directly |
500, TypeError as shown above |
apps/inline-isomorphic |
Writes createIsomorphicFn()... directly |
200, loader data is undefined |
They are split into three apps because inline-server-only throws during module evaluation and crashes the entire app, making it impossible to showcase 200 and 500 within the same app.
vite dev fails in the same way (the only difference is that the IDs become base64 strings instead of hashes).
Expected behavior
Since .handler(createServerOnlyFn(impl)) and const f = createServerOnlyFn(impl); .handler(f) represent the same JavaScript, they should compile to the same server function.
If the policy is to not support one of these patterns, the build should fail instead. Currently, it generates either a server function that throws on every request or one that silently returns undefined.
Screenshots or Videos
No response
Platform
- Router / Start Version:
@tanstack/react-start 1.168.54, @tanstack/start-plugin-core 1.171.44, @tanstack/react-router 1.170.36
- OS: macOS 26.6.2 arm64, Node 26.7.0, npm 11.19.0
- Browser: Not used
- Browser Version: N/A
- Bundler: vite
- Bundler Version: 8.3.0
Additional context
No response
Which project does this relate to?
Start
Describe the bug
If you pass a function call that the compiler is supposed to transform directly into the argument of createServerFn().handler(), the build will succeed, but it will generate a broken server function.
createServerOnlyFnAs a result, it will return 500.
Since it is thrown during module evaluation inside
loadEntries, it fails before routing, causing not only that route but the entire application to crash.createIsomorphicFnNo exception is thrown, but the server function returns
undefined.If you bind the call to a variable first and pass the identifier, both will be generated correctly.
Generated Code
Caller chunk (
dist/server/assets/router-*.js):The client bundle has the same broken slot, and in both cases, a local function is included instead of an RPC.
Cause
Candidates are collected in a single pass, and each holds a Babel path. The handlers then run in a fixed order defined by
BuiltInKindHandlerOrderinpackages/start-plugin-core/src/start-compiler/compiler.ts:ServerFnruns first, andhandleCreateServerFn.ts:445replaces the first argument of.handler().Subsequent handlers operate through the paths captured before this replacement. From there, they diverge into two cases:
handleEnvOnly.tsreadspath.node.arguments[0]. Since the node pointed to by the path is nowcreateSsrRpc("<id>"), what it retrieves is the string literal of the ID. This leaves.handler("<id>"), and the handler ofcreateServerFnattempts to assignextractedFn.methodto that string.handleCreateIsomorphicFn.tsretainsenvCallInfo.firstArgPath?.nodecaptured from the original tree, sopath.replaceWith(innerFn)overwrites the stub with the implementation itself. The RPC wiring disappears, and no exception is thrown.The root cause is the same in both cases: subsequent handlers are touching the node replaced by
ServerFnthrough stale paths.Binding it to a variable avoids this because the node for
createServerOnlyFn(...)moves to the initializer of the variable declaration, and none of the handlers replace it there.Complete minimal reproducer
https://github.com/imaimai17468/tanstack-start-handler-call-expression-repro
Steps to Reproduce the Bug
This builds three apps, displays the generated
.handler(...)indist/server, and startsvite previewso you can make a request to/.The results are as follows:
.handler()apps/hoistedconst wrapped = createServerOnlyFn(impl){"ranOn":"server"}apps/inline-server-onlycreateServerOnlyFn(impl)directlyTypeErroras shown aboveapps/inline-isomorphiccreateIsomorphicFn()...directlyundefinedThey are split into three apps because
inline-server-onlythrows during module evaluation and crashes the entire app, making it impossible to showcase 200 and 500 within the same app.vite devfails in the same way (the only difference is that the IDs become base64 strings instead of hashes).Expected behavior
Since
.handler(createServerOnlyFn(impl))andconst f = createServerOnlyFn(impl); .handler(f)represent the same JavaScript, they should compile to the same server function.If the policy is to not support one of these patterns, the build should fail instead. Currently, it generates either a server function that throws on every request or one that silently returns
undefined.Screenshots or Videos
No response
Platform
@tanstack/react-start1.168.54,@tanstack/start-plugin-core1.171.44,@tanstack/react-router1.170.36Additional context
No response