Duplicates
Latest version
Current behavior
ServerFunctionErrorHandler is typed (thrown: unknown) => unknown, so an async export typechecks. applyServerFunctionErrorHandler does not await it:
return onServerFunctionError?.(thrown) ?? thrown;
The promise is passed to seroval, which streams it. Two things follow:
- The client rejects with a
Promise, not the error. fetchServerFunction does const result = await extractBody(...) then throw result, and result is the deserialized promise, so a caller's catch receives a Promise object.
X-Error degrades from the error message to "true", because a promise is neither Error nor string.
It does not fail loudly. It half-works.
Expected behavior
Either the result is awaited, or an async handler is refused up front.
Steps to reproduce
Steps:
-
In apps/tests, make the module named by serverFunctions.onError export an async handler:
const onServerFunctionError: ServerFunctionErrorHandler = async thrown => {
await new Promise(resolve => setTimeout(resolve, 50));
return new Error("replaced by async onError");
};
-
Run pnpm dev and read the server function id from curl localhost:3000/src/<module>.ts.
-
Call a throwing server function over the network:
curl -i -X POST localhost:3000/_server -H "X-Server-Id: <id>" -H "X-Server-Instance: server-fn:1"
-
The response is 200 with x-error: true and a streamed promise:
;0x0000002d;{"t":22,"i":0,"s":1,...}
;0x0000016a;{"t":23,"i":1,"a":[...,{"t":13,"m":"replaced by async onError"}]}
With a sync handler the same call returns x-error: replaced by onError.
Context
I added this option in #2262, so this is my own sharp edge. Reporting to a monitoring service is the option's main use, and several SDKs want an await to flush before a serverless process is torn down, so reaching for async is the natural first attempt.
Two directions, happy to send a PR for whichever you prefer:
-
Await it. handleServerFunction is already async, so x = await applyServerFunctionErrorHandler(x) is a one-line change and makes await flush() work. Cost: a handler can no longer return a promise as the error value.
-
Refuse it. Keep the sync contract and make the mistake impossible:
type NotPromise<T> = T extends PromiseLike<unknown> ? never : T;
export type ServerFunctionErrorHandler = <T>(thrown: unknown) => NotPromise<T>;
plus a dev-only runtime guard in applyServerFunctionErrorHandler that throws when the result is thenable, so a JavaScript user or a cast is told rather than shipping the streamed promise.
Your environment
System:
OS: macOS (Darwin 24.6.0)
Binaries:
Node: 24.14.1
pnpm: 11.17.0
npmPackages:
@solidjs/start: 2.0.0-rc.6 (solid-start main @ d34cb89)
Duplicates
Latest version
Current behavior
ServerFunctionErrorHandleris typed(thrown: unknown) => unknown, so anasyncexport typechecks.applyServerFunctionErrorHandlerdoes not await it:The promise is passed to seroval, which streams it. Two things follow:
Promise, not the error.fetchServerFunctiondoesconst result = await extractBody(...)thenthrow result, andresultis the deserialized promise, so a caller'scatchreceives aPromiseobject.X-Errordegrades from the error message to"true", because a promise is neitherErrornorstring.It does not fail loudly. It half-works.
Expected behavior
Either the result is awaited, or an
asynchandler is refused up front.Steps to reproduce
Steps:
In
apps/tests, make the module named byserverFunctions.onErrorexport an async handler:Run
pnpm devand read the server function id fromcurl localhost:3000/src/<module>.ts.Call a throwing server function over the network:
The response is
200withx-error: trueand a streamed promise:With a sync handler the same call returns
x-error: replaced by onError.Context
I added this option in #2262, so this is my own sharp edge. Reporting to a monitoring service is the option's main use, and several SDKs want an
awaitto flush before a serverless process is torn down, so reaching forasyncis the natural first attempt.Two directions, happy to send a PR for whichever you prefer:
Await it.
handleServerFunctionis already async, sox = await applyServerFunctionErrorHandler(x)is a one-line change and makesawait flush()work. Cost: a handler can no longer return a promise as the error value.Refuse it. Keep the sync contract and make the mistake impossible:
plus a dev-only runtime guard in
applyServerFunctionErrorHandlerthat throws when the result is thenable, so a JavaScript user or a cast is told rather than shipping the streamed promise.Your environment