Summary
withResult discards the detail of any non-Error throw, because no call site in the repo supplies the hook that would preserve it.
The mechanism
@byteslice/result@0.2.0, dist/result.js:
function ensureError(ex) {
return ex instanceof Error ? ex : new Error("Something went wrong");
}
async function withResult(operation, onError, hooks) {
try {
return { data: await operation() };
} catch (ex) {
const error = hooks?.onException?.(ex) ?? ensureError(ex);
return { failure: onError(error) };
}
}
ensureError is only the fallback. The third parameter — hooks?: { onException?: (ex: unknown) => Error } — receives the raw exception and takes precedence.
$ grep -rn "onException" packages/ --include='*.ts' | grep -v node_modules
(no matches)
So every withResult in the repo silently converts a non-Error rejection into Error("Something went wrong"), and the onError mapper — e.g. (error as Error).message in the native operations — faithfully records the boilerplate.
Impact
Wherever a non-Error value can be thrown, the failure message a caller sees is "Something went wrong" with no way to recover what actually happened. It is not a hypothetical shape: packages/stack/src/dynamodb/operations/* explicitly notes that "withResult's ensureError wraps non-Error objects", which is why those paths hand-roll throwPreservingCode to keep the FFI code alive.
Most acute at any WASM/FFI boundary — see the fix already landed for wasm-inline in #741, where wasm-bindgen rejects with the raw JsValue and the WASM build exports no ProtectError class, so real failures genuinely arrive as bare strings or objects.
Fix
Pass onException at the withResult call sites, converting without losing the value — roughly what wasm-inline.ts's toError now does:
function toError(ex: unknown): Error {
if (ex instanceof Error) return ex
if (typeof ex === 'string') return new Error(ex)
try {
return new Error(JSON.stringify(ex) ?? String(ex))
} catch {
return new Error(String(ex))
}
}
Worth doing once as a shared helper rather than per call site — the omission is invisible (it does not fail a build, it just degrades messages), so a single seam that binds the hook is what stops it recurring.
Related
Summary
withResultdiscards the detail of any non-Errorthrow, because no call site in the repo supplies the hook that would preserve it.The mechanism
@byteslice/result@0.2.0,dist/result.js:ensureErroris only the fallback. The third parameter —hooks?: { onException?: (ex: unknown) => Error }— receives the raw exception and takes precedence.So every
withResultin the repo silently converts a non-Errorrejection intoError("Something went wrong"), and theonErrormapper — e.g.(error as Error).messagein the native operations — faithfully records the boilerplate.Impact
Wherever a non-
Errorvalue can be thrown, the failure message a caller sees is"Something went wrong"with no way to recover what actually happened. It is not a hypothetical shape:packages/stack/src/dynamodb/operations/*explicitly notes that "withResult'sensureErrorwraps non-Error objects", which is why those paths hand-rollthrowPreservingCodeto keep the FFI code alive.Most acute at any WASM/FFI boundary — see the fix already landed for
wasm-inlinein #741, where wasm-bindgen rejects with the rawJsValueand the WASM build exports noProtectErrorclass, so real failures genuinely arrive as bare strings or objects.Fix
Pass
onExceptionat thewithResultcall sites, converting without losing the value — roughly whatwasm-inline.ts'stoErrornow does:Worth doing once as a shared helper rather than per call site — the omission is invisible (it does not fail a build, it just degrades messages), so a single seam that binds the hook is what stops it recurring.
Related
wasm-inlineentry, where the hazard is sharpest. The same gap remains on every otherwithResultcall site.@byteslice/result0.2.0 → 0.5.0 may changeensureError's default, but passingonExceptionis correct regardless and does not depend on that landing.