Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved sfError #1046

Merged
merged 7 commits into from
Apr 8, 2024
Merged

feat: improved sfError #1046

merged 7 commits into from
Apr 8, 2024

Conversation

mshanemc
Copy link
Contributor

@mshanemc mshanemc commented Apr 4, 2024

What does this PR do?

  • bump tsconfig target/lib/etc for es2022 so we get native error.cause
  • sfError extends Error instead of kit/NamedError
    • adds the name property
    • adds the fullStack method...which now operates on stack of error.cause by recursing through them to build its "caused by"
    • add a new .create method that's less painful than the 5-prop overloaded constructor and which supports the context and data properties
    • wrap method takes an unknown so you can wrap things in a catch block without having to worry about their type (previously only supported string | Error | SfError). Will wrap any "other" things in an error to preserve its info in the new error's cause
    • the constructor (arg5) and the new .create also allow cause to be unknown and will narrow it to an error (and otherwise throw) for that same convenience in catch blocks.
    • code property always returns a string [it's either code (which can only be a string) or name (also required, string)] and now the types say that

What issues does this PR fix or reference?

@W-14507125@

import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types';

export type SfErrorOptions<T extends ErrorDataProperties = ErrorDataProperties> = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

type for the new .create method param obj

actions?: string[];
};

type ErrorDataProperties = AnyJson;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

constrain the data Prop...see the "DANGER" note on the original's line 145

@@ -24,7 +35,8 @@ import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types';
* throw new SfError(message.getMessage('myError'), 'MyErrorName');
* ```
*/
export class SfError<T = unknown> extends NamedError {
export class SfError<T extends ErrorDataProperties = ErrorDataProperties> extends Error {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

leave NamedError out of this

@@ -24,7 +35,8 @@ import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types';
* throw new SfError(message.getMessage('myError'), 'MyErrorName');
* ```
*/
export class SfError<T = unknown> extends NamedError {
export class SfError<T extends ErrorDataProperties = ErrorDataProperties> extends Error {
public readonly name: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

brought from NamedError

src/sfError.ts Outdated Show resolved Hide resolved
/**
* Convert an Error to an SfError.
*
* @param err The error to convert.
*/
public static wrap(err: Error | string): SfError {
public static wrap<T extends ErrorDataProperties = ErrorDataProperties>(err: unknown): SfError<T> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

supports unknown.

}

if (err instanceof SfError) {
return err;
return err as SfError<T>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

only matters if you want to type the .data prop

@@ -5,6 +5,10 @@
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true
"esModuleInterop": true,
"lib": ["ES2022"],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

necessary to get the native error.cause

Copy link
Contributor Author

@mshanemc mshanemc Apr 4, 2024

Choose a reason for hiding this comment

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

we already set engines to node18+ but now we type/compile for it. Dev-scripts non-esm are still on es2021.

Copy link
Member

@WillieRuemmele WillieRuemmele left a comment

Choose a reason for hiding this comment

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

it all seems like a lot of usability improvements

const mySfError = SfError.wrap(wrapMe);
expect(mySfError).to.be.an.instanceOf(SfError);
expect(mySfError.message === 'An unexpected error occurred');
expect(mySfError.name === 'TypeError');
Copy link
Member

Choose a reason for hiding this comment

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

Does it really make sense than an SfError.wrap(undefined).name==='TypeError'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think so. You're passing something that is of an unexpected type and we can't do much with it.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError

A TypeError may be thrown when:
an operand or argument passed to a function is incompatible with the type expected by that operator or function; or

What would you prefer?

src/sfError.ts Outdated
? // a basic error with message and name. We make it the cause to preserve any other properties
new SfError<T>(err.message, err.name, undefined, err)
: // ok, something was throws that wasn't error or string. Convert it to an Error that preserves the information as the cause and wrap that.
SfError.wrap<T>(new TypeError('An unexpected error occurred', { cause: err }));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why TypeError and not just Error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see above comment about TypeError. Sounds like it's confusing to 2 people now.

I'd argue it's fairly idiomatic in TS (typically, type-checking an any/unknown)
see https://github.com/search?q=throw+new+TypeError+language%3ATypeScript&type=code&l=TypeScript

Copy link
Contributor

Choose a reason for hiding this comment

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

To me, a TypeError means that the author of the code made a mistake and someone needs to fix a client side bug. Usually this would mean the CLI team. W.r.t "plugin health" it would be categorized as a bug for the CLI team (if called from the sf CLI). If this is intended, then it's fine. If the meaning of the error is anything else, we should just use Error. Although, since it's immediately being wrapped by SfError the command framework probably wouldn't interpret this as a TypeError and would categorize it differently. In the end it probably doesn't matter much.

super(name ?? 'SfError', message || name, cause);
if (typeof cause !== 'undefined' && !(cause instanceof Error)) {
throw new TypeError(`The cause, if provided, must be an instance of Error. Received: ${typeof cause}`);
}
Copy link
Member

Choose a reason for hiding this comment

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

related to the WI about correctly bubbling up the erro from iso-git > STL > SDR:

STL throws an SfError with data in it, SDR will wrap it and rethrow (new SfError...) but you have to remember to call error.setData(originalError) or the data is lost.
Could we check here in the constructor if cause instanceof SfError and save possible cause.data in the new error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a "parent" error could call setData or mutating the data prop and losing the cause data.

but, it's a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm....there's also the typing for data to be concerned about.

Ex:

if (resolvedCause instanceof SfError && resolvedCause.data) {
      this.data = resolvedCause.data; <== this is all squiggly 
    }

because the SfError can be constructed with a but the cause has a different and unknowable T. Asserting that it is the same can cause (no pun intended) problems upstream.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but maybe this is the least bad option.

 this.data = resolvedCause.data as T;

@mshanemc mshanemc merged commit 9b4ac85 into sm/core-7-deprecations Apr 8, 2024
66 checks passed
@mshanemc mshanemc deleted the sm/sfError branch April 8, 2024 19:10
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.

5 participants