-
Notifications
You must be signed in to change notification settings - Fork 69
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
feat: improved sfError #1046
Conversation
import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types'; | ||
|
||
export type SfErrorOptions<T extends ErrorDataProperties = ErrorDataProperties> = { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
brought from NamedError
/** | ||
* 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> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supports unknown.
- Safer from runtime errors from not checking types in a catch like ex:
Line 1599 in 092af1b
const err = SfError.wrap(e as string | Error); - Simpler than checking types in a catch block like
sfdx-core/src/org/scratchOrgInfoApi.ts
Lines 379 to 380 in 092af1b
if (error instanceof Error) { const sfError = SfError.wrap(error);
} | ||
|
||
if (err instanceof SfError) { | ||
return err; | ||
return err as SfError<T>; |
There was a problem hiding this comment.
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"], |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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
test/unit/sfErrorTest.ts
Outdated
const mySfError = SfError.wrap(wrapMe); | ||
expect(mySfError).to.be.an.instanceOf(SfError); | ||
expect(mySfError.message === 'An unexpected error occurred'); | ||
expect(mySfError.name === 'TypeError'); |
There was a problem hiding this comment.
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'
?
There was a problem hiding this comment.
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 })); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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}`); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;
What does this PR do?
name
propertyerror.cause
by recursing through them to build its "caused by".create
method that's less painful than the 5-prop overloaded constructor and which supports thecontext
anddata
propertiesunknown
so you can wrap things in acatch
block without having to worry about their type (previously only supportedstring | Error | SfError
). Will wrap any "other" things in an error to preserve its info in the new error'scause
.create
also allowcause
to beunknown
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 thatWhat issues does this PR fix or reference?
@W-14507125@