Skip to content

feat(ses): add support for "code" prop in SES-managed Errors#3130

Open
boneskull wants to merge 3 commits into
masterfrom
boneskull/error-code
Open

feat(ses): add support for "code" prop in SES-managed Errors#3130
boneskull wants to merge 3 commits into
masterfrom
boneskull/error-code

Conversation

@boneskull

Copy link
Copy Markdown
Member
  • Adds a new string option to AssertMakeErrorOptions, code
  • If a string, will be an non-enumerable property of the Error instance returned by makeError.
  • To avoid mutating global types, a new type SesError is introduced which is simply Error & {code?: string}.

Note that code is expected to be a string here, but only by convention; the language will not enforce this (as per the below proposal).

Ref: https://github.com/tc39-transfer/proposal-error-code-property

@changeset-bot

changeset-bot Bot commented Mar 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1ec13b7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@boneskull boneskull requested review from erights and gibson042 March 17, 2026 03:26
@boneskull boneskull self-assigned this Mar 17, 2026
@boneskull boneskull added the enhancement New feature or request label Mar 17, 2026
@boneskull boneskull requested review from kriskowal and naugtur March 17, 2026 03:26
@boneskull

boneskull commented Mar 17, 2026

Copy link
Copy Markdown
Member Author

What I'd really like to see here is a makeError which is generic on the error constructor type (so that it would return a value of that type), but the signature is such that it is not possible--the default parameter of globalThis.Error creates a type error when it's generic, and removing the default value is invalid syntax (since the previous message/details parameter also has a default value).

const makeError = (
optDetails = redactedDetails`Assert failed`,
errConstructor = globalThis.Error,
{
errorName = undefined,
cause = undefined,
errors = undefined,
sanitize = true,
code = undefined,
} = {},
) => {
// Promote string-valued `optDetails` into a minimal DetailsParts

The workaround is to just wrap makeError in another function.

@boneskull boneskull force-pushed the boneskull/error-code branch from e6d46c7 to 0b2ff5a Compare March 17, 2026 03:32
Comment thread packages/ses/types.d.ts Outdated
/**
* An `Error` with a `code` property.
*/
export type SesError = Error & { code?: string };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Open to naming suggestions

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(We could have used module augmentation to extend the global Error interface with a code?: string prop, but that would affect all downstream consumers of ses, which would be inconsiderate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Under any name, I don't like introducing a new type for this purpose if possible.

What about cause and errors? Do they have this problem too? If not, can we treat code the same way? Once we support SuppressedError (hopefully soon) we will also need to support the further instance properties error and suppressed.

@boneskull boneskull Mar 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

They don't have the same problem, because Error.cause and AggregateError.errors are in the TS lib.

Our options are:

  1. Create a new type (as this PR does)
  2. Augment the global Error type with a code prop
    This is hazardous because it will affect downstream consumers; anyone pulling in ses will have a global Error with a code prop on it. If we do not publish the augmentation, types will work in a development environment, but will be broken downstream.
  3. Fudge it with // @ts-expect-error everywhere code is referenced (gross)

I'm not sure what TS' policy is on adopting proposals (if they have one), but I think it's probably too early to ask.


That being said, code will/should/might end up in the TS lib eventually, at which point we can refactor away the custom type and/or alias it to Error.

@kriskowal kriskowal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs changeset and consensus on names.

Comment thread packages/ses/types.d.ts Outdated
@boneskull boneskull force-pushed the boneskull/error-code branch from c58a919 to bf477a0 Compare March 17, 2026 23:34
Comment thread packages/ses/src/error/assert.js Outdated
Comment thread packages/ses/src/error/assert.js Outdated
Comment thread packages/ses/types.d.ts Outdated
/**
* An `Error` with a `code` property.
*/
export type SesError = Error & { code?: string };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Under any name, I don't like introducing a new type for this purpose if possible.

What about cause and errors? Do they have this problem too? If not, can we treat code the same way? Once we support SuppressedError (hopefully soon) we will also need to support the further instance properties error and suppressed.

@erights

erights commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

See also DRAFT PRs #2223 and #2429 . Like them, we need to coordinate this change with @endo/pass-style and @endo/marshal so that .code is actually passed. Doesn't need to happen in this PR, but that is my preference since it should be a coordinated change. Please borrow as much as you like from those other draft PRs.

@boneskull

boneskull commented Mar 24, 2026

Copy link
Copy Markdown
Member Author

See also DRAFT PRs #2223 and #2429 . Like them, we need to coordinate this change with @endo/pass-style and @endo/marshal so that .code is actually passed. Doesn't need to happen in this PR, but that is my preference since it should be a coordinated change. Please borrow as much as you like from those other draft PRs.

@erights What should I be looking at there? I'm unfamiliar with both of those packages and any discussions around either PR.

I'm operating under the assumption that this PR only satisfies the (fairly minimal) requirements of @endo/compartment-mapper so that it can be consumed sooner rather than later.

I'd expect we'd extend the implementation and/or adopt in other packages when ready. Are you concerned about shipping this in ses as-is? If so, would you be amenable to moving the implementation into @endo/compartment-mapper temporarily so it can be trialed in that context, and then re-integrating with ses in the future?

@boneskull boneskull force-pushed the boneskull/error-code branch 4 times, most recently from bf3c506 to f895a53 Compare March 30, 2026 01:38
@mhofman

mhofman commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

FYI, I found myself in need of creating errors with a .code, so I overrode and created extra helpers around @endo/errors

In particular I'm adding an XError helper to create Error using template literals like Fail does but without throwing (a wrapper around makeError but without the constructor and details cruft), and make both this XError and Fail chainable to define the options that makeError accepts (credit to @gibson042 for the idea).

errors.ts:
import { makeError as originalMakeError, X } from "@endo/errors";

export * from "@endo/errors";

type Details = NonNullable<Parameters<typeof originalMakeError>[0]>;
type GenericErrorConstructor = NonNullable<
  Parameters<typeof originalMakeError>[1]
>;
type AssertMakeErrorOptions = NonNullable<
  Parameters<typeof originalMakeError>[2]
> & {
  code?: string;
};

type ChainedErrorMaker<T> = ((
  template: TemplateStringsArray | string[],
  ...args: any[]
) => T) & {
  code(code: string): ChainedErrorMaker<T>;
  instance(ctor: GenericErrorConstructor): ChainedErrorMaker<T>;
  options(options: AssertMakeErrorOptions): ChainedErrorMaker<T>;
};

const assertFailedDetails = X`Check failed`;

export const makeError = (
  details: Details = assertFailedDetails,
  errConstructor: GenericErrorConstructor | undefined = undefined,
  options: AssertMakeErrorOptions | undefined = undefined,
): Error => {
  const { code, sanitize = false, ...makeErrorOptions } = options ?? {};
  const hasCode = code !== undefined;
  const error = originalMakeError(details, errConstructor, {
    ...makeErrorOptions,
    // `code` and `sanitize` are mutually exclusive options, ignore sanitize
    sanitize: !hasCode && sanitize,
  });
  if (code !== undefined) {
    Object.defineProperty(error, "code", {
      value: code,
      enumerable: false,
      configurable: true,
      writable: true,
    });
  }
  return error;
};

export const fail = (
  /** The details of what was asserted */
  details: Details = assertFailedDetails,
  /** An optional alternate error constructor to use */
  errConstructor: GenericErrorConstructor | undefined = undefined,
  options: AssertMakeErrorOptions | undefined = undefined,
): never => {
  throw makeError(details, errConstructor, options);
};

const makeChainedHelper = <T>(
  fn: (...args: Parameters<typeof makeError>) => T,
) => {
  const name = fn.name && fn.name.charAt(0).toUpperCase() + fn.name.slice(1);
  const maker = (
    options: AssertMakeErrorOptions = {},
    constructor?: GenericErrorConstructor | undefined,
  ): ChainedErrorMaker<T> => {
    // XXX: Consider defaulting to carrying over the code of any error passed in values
    // with the caveat that such value may be classified (quoted or bare)
    const Fail: ChainedErrorMaker<T> = (template, ...values) =>
      fn(X(template, ...values), constructor, options);
    Fail.code = (code: string) => maker({ ...options, code }, constructor);
    Fail.instance = (ctor: GenericErrorConstructor) => maker(options, ctor);
    Fail.options = (opts: AssertMakeErrorOptions) =>
      maker({ ...options, ...opts }, constructor);
    Object.defineProperty(Fail, "name", { value: name });
    return Fail;
  };
  return maker();
};

export const Fail = makeChainedHelper(fail);

export const XError = makeChainedHelper(makeError);

@boneskull

Copy link
Copy Markdown
Member Author

FYI, I found myself in need of creating errors with a .code, so I overrode and created extra helpers around @endo/errors

In particular I'm adding an XError helper to create Error using template literals like Fail does but without throwing (a wrapper around makeError but without the constructor and details cruft), and make both this XError and Fail chainable to define the options that makeError accepts (credit to @gibson042 for the idea).

errors.ts:

@mhofman Wait does this mean I can rewrite @endo/compartment-mapper in TS?? 😄

@mhofman

mhofman commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

@mhofman Wait does this mean I can rewrite @endo/compartment-mapper in TS?? 😄

When #3137 lands, yes

@boneskull

boneskull commented Apr 5, 2026

Copy link
Copy Markdown
Member Author

📚 Pull Request Stack


Managed by gh-stack

@boneskull boneskull force-pushed the boneskull/error-code branch 2 times, most recently from 62ebce3 to 347e386 Compare April 14, 2026 02:19
@boneskull boneskull force-pushed the boneskull/error-code branch 2 times, most recently from 75b5a7f to 00d996f Compare April 22, 2026 04:14
@boneskull boneskull force-pushed the boneskull/error-code branch from 00d996f to 6b968ca Compare April 23, 2026 01:42
@boneskull boneskull force-pushed the boneskull/error-code branch 4 times, most recently from 8df7a6a to c3c9392 Compare May 5, 2026 20:07
@boneskull boneskull force-pushed the boneskull/error-code branch 2 times, most recently from 3d9cda2 to 703e928 Compare May 12, 2026 23:27
@boneskull boneskull force-pushed the boneskull/error-code branch 7 times, most recently from 5efc309 to e8f139c Compare May 26, 2026 02:23
@boneskull boneskull force-pushed the boneskull/error-code branch 3 times, most recently from 547293a to 9b2f8f9 Compare June 3, 2026 23:04
@boneskull boneskull force-pushed the boneskull/error-code branch from 9b2f8f9 to 97810ee Compare June 5, 2026 00:59
@boneskull boneskull force-pushed the boneskull/error-code branch from 97810ee to cf1c4a2 Compare June 12, 2026 01:11
- Adds a new `string` option to `AssertMakeErrorOptions`, `code`
- If a `string`, will be an non-enumerable property of the `Error` instance returned by `makeError`.
- To avoid mutating global types, a new type `SesError` is introduced which is simply `Error & {code?: string}`.

Note that `code` is expected to be a `string` here, but only by convention; the language will not enforce this (as per the below proposal).

Ref: https://github.com/tc39-transfer/proposal-error-code-property
has nothing to do with fish
@boneskull boneskull force-pushed the boneskull/error-code branch from cf1c4a2 to 1ec13b7 Compare June 12, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants