-
Notifications
You must be signed in to change notification settings - Fork 764
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
Refactor errors to ES6 classes #661
Conversation
Wow, this is awesome; thank you @tinovyatkin ! At first glance looks great. Going to want to think about whether this could result in any unintended breaking changes for users, or other unintended consequences... (Can you think of anything?) I'm also curious if we should hold off on this until we're able to change everything to uses proper classes. Hope to give this a more thorough think/look soon – hopefully today, but may not be until early next week. |
As I wrote initially, I've selected errors classes as a relatively small non-breaking change and where class refactoring in new code will really stands out (imho version with classes looks way more beautiful, and that what is Stripe all about, right? ;-)). Not sure about Stripe policy, but I love small PRs, and refactoring everything to class will be a huge task. You have two candidates left for class refactoring - |
"chai": "~4.2.0", | ||
"chai-as-promised": "~7.1.1", | ||
"coveralls": "^3.0.0", | ||
"eslint": "^5.16.0", | ||
"eslint-config-prettier": "^4.1.0", | ||
"eslint-plugin-chai-friendly": "^0.4.0", | ||
"eslint-plugin-flowtype": "^3.8.1", |
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.
good catch, not sure how that got in there...
Thanks, yes, this does make the code more beautiful and we do appreciate that 😄 We do consider some undocumented features to be backwards-breaking, especially if we think there's a reasonable chance that reasonable users may have relied on undocumented behavior. I am hopeful that in this case we'll be okay. If we don't merge this in now, though, I can certainly create a long-running branch for ES6 classes and merge it into that. We do this fairly frequently to keep PR's small while batching major changes. |
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.
This looks really great.
I definitely think some users may have written code like this:
const {StripeError, StripeCardError} = require('stripe/lib/Error')
const MyStripeError = StripeError.extend({type: 'MyError'})
const My StripeCardError = StripeCardError.extend({
type: 'MyCardError',
populate(raw) {
this.detail = "hello"
this.customField = "hi"
}
})
Can we add a shim with tests demonstrating that these work?
|
||
_Error.extend = utils.protoExtend; |
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 this is the function most likely to have been used by folks outside of Stripe, if for some reason they decided to define their own errors as an extend
of some StripeError. Is there a way to preserve it?
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.
@rattrayalex-stripe did as much as possible (the only case that will be breaking now is default export that is not a function itself). We can do even that, but it will look really weird and for very edge use case.
Weird, I think github may be eating some of my comments? But I think we can actually revert most of f3528b4 and just keep this (plus the tests): /**
* DEPRECATED
* Please use ES6 class inheritance instead.
* @param {{ type: string, message?: string, [k:string]: any }} options
*/
static extend(options) {
class DeprecatedCustomError extends GenericError {}
DeprecatedCustomError.type = options.type;
for (const property in options) {
DeprecatedCustomError.prototype[property] = options[property];
}
return DeprecatedCustomError;
} unless I'm missing something? Sadly, I also think we do need to support this; edge case-y though it may be, it's part of our API: const Error_ = require('stripe/lib/Error')
Error_.extend({type: 'Hi'}) |
Fixed breaking change, and found a nice workaround with |
test/Error.spec.js
Outdated
const e = new Error('FooError', 'Foo happened'); | ||
expect(e).to.have.property('type', 'FooError'); | ||
const e = new Error.GenericError('Foo happened'); | ||
expect(e).to.have.property('type', 'GenericError'); |
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.
Can this test change be reverted? I think this test should continue to pass.
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.
Ahhh, I see, this is what you were talking about. The Error exported from the root has a totally different signature from GenericError
, and also doesn't make much sense to extend... hmm...
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 should probably keep all the code for function _Error(raw) {
and just mark it as deprecated (and not actually inherit from it in the ES6 classes). Thoughts?
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.
Error
here is default import from Error.js
. To make it pass we should be doing weird things as I commented earlier, like this:
module.exports = GenericError;
module.exports.StripeError = StripeError;
I mean, export GenericError
with other errors as attached properties on it...
The example you put makes sense, extend
export do work, but doing this will look waoh...
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'm thinking this:
module.exports = _Error;
module.exports.StripeError = class StripeError {...}
module.exports.StripeFooError = class StripeFooError extends StripeError;
/**
* DEPRECATED
* This is here for backwards compatibility and will be removed in the next major version.
*/
function _Error(raw) {
this.populate(...arguments);
this.stack = new Error(this.message).stack;
}
_Error.prototype = Object.create(Error.prototype);
_Error.prototype.type = 'GenericError';
_Error.prototype.populate = function(type, message) {
this.type = type;
this.message = message;
};
_Error.extend = utils.protoExtend;
...
Yes, the legacy code is ugly; let's just shunt it to the bottom, mark it as deprecated, and not have the rest of our classes build on it.
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.
Weeeell... Can't do THAT ugly. Passing all original tests now ✅
lib/Error.js
Outdated
} | ||
class GenericError extends Error { | ||
constructor(raw) { | ||
super(arguments[1] || raw.message); |
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, I think this isn't quite right...
Would have unexpected behavior in cases like these
new StripeError(false, 'a string')
new StripeError('a string')
new StripeError({some: 'object'}, {another: 'object'})
eg; I think this might set type or even throw in places that the old way would not have...
getting super edge-casey here of course.
I think this could maybe be solved by overriding the constructor to be more sane in StripeError, or with another layer of indirection...
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.
That's really super edge cases, particularly because original implementation doesn't do any typecheck and we can run in the same problems. In Javascript you always can construct an object with some property getter/setter or well known symbol implementation that will throw any function.
But ok. What is the expected behavior in your examples? To throw or to ignore invalid parameters?
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.
Assuming expected behavior is like a standard errors (never throw, use as much information from params as possible) I've added typechecks and edge cases tests.
It doesn't make code any more beautiful, but bulletproof!
@tinovyatkin I really hate to be difficult here, but I'm uncomfortable having our base error class contain that sort of tangled logic for a deprecated, legacy edge-case. I'd really like to disentangle the two. My suggestion here still feels much better than this, though you're welcome to implement the same idea with classes if you like? All that said, I'm impressed and grateful for all your hard work here, and don't want to impose further on your time – I know I've asked a lot here. I'm happy to merge this as-is and make that change myself if you like? Up to you! |
@rattrayalex-stripe I really don't want to have mixed in the same file use of My last attempt here is to return constructor to |
Okay, totally fair 😄you've done a ton here already it's much appreciated. I think it'd be pretty unfair for me to ask even more of you, and I'm excited to see the bulk of this land! LGTM I'm going to want to make some adjustments before releasing this, so I'll hold off on merging until my own work is ready. |
Your Node.JS requirements at
package.json
is 6.x as minimum, and it's fully supports ES6 classes. Your are using legacy extending everywhere, so, I think errors classes as smallest and non-breaking change will be good foundation for migration.Forced to adjust ESLint config, as rule was conflicting with JSDoc comments for constructor and Prettier. And while we was there, removed unused
flowtype
ESlint plugin and unneededbabel-eslint
(ESLint since version 5.x supports all that syntax)I would love to refactor
stripe.js
into real class, but it's gonna be a breaking change, so, didn't touch for now.