Skip to content

withResult: no call site passes onException, so non-Error throws lose their message repo-wide #743

Description

@coderdan

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

Metadata

Metadata

Assignees

Labels

SDKbugSomething isn't working

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions