Skip to content

Preserve own properties of Error instances across serialization/deserialization#166

Open
aron-cf wants to merge 1 commit intocloudflare:mainfrom
aron-cf:error-attributes
Open

Preserve own properties of Error instances across serialization/deserialization#166
aron-cf wants to merge 1 commit intocloudflare:mainfrom
aron-cf:error-attributes

Conversation

@aron-cf
Copy link
Copy Markdown
Contributor

@aron-cf aron-cf commented Apr 30, 2026

Closes #87.

Today the wire format for Error only carries name, message, and (optionally) stack. Anything a user attaches to the error — code, details, a cause, the inner errors of an AggregateError — is lost the moment the error crosses an RPC boundary.

This PR extends the error expression with an optional fifth element, props. This uses the same serialization/deserialization as any other object.

Error.cause and AggregateError.errors are normally non-enumerable, so they are picked up explicitly by the encoder. On the receive side, the decoder constructs the error as before and then assigns each entry from props as an own enumerable property on the result.

let err = new Error("rate limited");
err.code = "E_LIMIT";
err.retryAfter = 30;
throw err;
// receiver sees: err.code === "E_LIMIT", err.retryAfter === 30

[!NOTE}
Individual property values that cannot be represented on the wire — capabilities without a session source, cycles, Object.create(null), symbols, and so on — are silently dropped from props rather than aborting the whole error. Callers who want to scrub heavy or sensitive fields explicitly can still do so from onSendError, which continues to run before any property capture.

I would appreciate feedback on this, it may be simpler to just drop the property entirely if any field fails to serialize.

The wire format change is fully backwards-compatible.

Tests have been added to verify the serialization/deserialization of various different error cases and failure modes. There may be too many, happy to remove any if needed.

The error expression in protocol.md now documents the optional props element, the lossy-fallback semantics, and the backwards-compatibility rule.

This commit ensures that own properties are sent across the RPC
boundary and reconstructed on the Error instance on the other side.

The `cause` field and, in the case of `errors` for `AggregateError`
are also copied over.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

🦋 Changeset detected

Latest commit: 13491be

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
capnweb Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 30, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/capnweb@166

commit: 13491be

Comment on lines +17 to +19
Property values that cannot be represented over the wire (capabilities without a session, cycles, unsupported types) are silently dropped from the error's property bag; the error itself always reaches the receiver. Use `onSendError` to scrub fields explicitly if you need control over what gets sent.

The wire format change is backwards-compatible: errors with no extras are still emitted in the legacy 3- or 4-element form, and older peers that only understand that form will simply ignore the new trailing element.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Happy to make this shorter if we want terser changelog entries.

Comment thread src/serialize.ts
Comment on lines +308 to +311
// supported type round-trips. If a property's value can't be serialized (a capability
// without a session, a cycle, an Object.create(null), …) we silently drop it: the
// error itself must always make it through. Use `onSendError` to scrub heavy or
// sensitive fields explicitly.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Would appreciate feedback on whether we just drop the entire property if it fails to serialize rather than making a best effort attempt.

Comment thread protocol.md
Comment on lines +189 to +191
When `props` is present, `stack` is normalised to `null` if absent so that positional indexing for `props` is unambiguous. When there are no extras, the legacy 3- or 4-element form is emitted unchanged.

The `props` element is backwards-compatible: a peer that only understands the 4-element form ignores it and recovers the standard `name`/`message`/`stack`. A peer that understands `props` but receives the 3- or 4-element form simply sees no extras.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Happy to remove this if it's too much information but felt it might be useful to have documented.

@aron-cf aron-cf marked this pull request as ready for review April 30, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom error details

1 participant