-
Notifications
You must be signed in to change notification settings - Fork 3k
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(error): Lazy Error.stack usage #2781
Conversation
55f0a45
to
ff69803
Compare
Generated by 🚫 dangerJS |
ff69803
to
dfcf919
Compare
We have actually changed our custom error (targeted which probably doesn't require this changes. This is interesting approach, but we could probably just cherry-pick those changes only from |
Yes, those setPrototypeOf calls are fine as well! Good alternative. I don't think it is breaking: the same properties are exposed, the errors don't change. There is some other stuff in there too though. A careful cherry-pick, including only the non-nexty changes, would be fine. I can rebase this PR to such a cherry-pick on Monday. |
7b94eb9
to
bb068c5
Compare
Never mind. The proposed changes in next are wrong: I added a test for this in the last commit. I had no idea Errors where so complicated in JavaScript. See for example https://gist.github.com/justmoon/15511f92e5216fa2624b#anti-patterns. |
If changes in |
Agreed... I'm writing some tests as we speak. Unfortunately the PR #2614 is a little messed up because next is merged in back instead of rebasing. Therefore cherry-picking that cleanly is not easy. That complicates the backporting part... |
bb068c5
to
809656c
Compare
TypeScript 2.4 is more strict about assignability with generics, and in some cases (when calling a generic function) it will infer that the generic parameter is <{}> rather than the <T> that rxjs typically wants. Fix this by explicitly passing <T> in the cases where compilation fails.
Error.stack can potentially trigger expensive prepareStackTrace calls, Observable.timeout would crate a TimeoutError on use, before the actual error occurs. This eases tracing the location of the timeout, but with the current implementation, slows down the application when using many .timeout operators throughout the code.
809656c
to
a38bfe6
Compare
I suddenly have more time to invest in RxJS.. because it's actually my job now... I think this is something we're interested in doing. It does need more review, but it also needs refactored since files moved around. |
@benlesh I've had a look at this PR with a view to getting it into a state in which it can be applied to v6. However, I'm not convinced that it's required. In fact, I think the current, v6 implementation should be preferred. Using TypeScript 2.8.3, the following code: class SomeError extends Error {
private err: Error;
constructor() {
const err: any = super("some error");
this.err = err;
}
get stack(): string | undefined {
return this.err.stack;
}
}
const s = new SomeError();
console.log(`has stack = ${Boolean(s.stack)}`);
console.log(`instanceof Error = ${s instanceof Error}`);
console.log(`instanceof SomeError = ${s instanceof SomeError}`); outputs this:
Whereas this code: class AnotherError extends Error {
constructor() {
super("another error");
(Object as any).setPrototypeOf(this, AnotherError.prototype);
}
}
const a = new AnotherError();
console.log(`has stack = ${Boolean(a.stack)}`);
console.log(`instanceof Error = ${a instanceof Error}`);
console.log(`instanceof AnotherError = ${a instanceof AnotherError}`); outputs this:
Note that the implementation that uses this PR's mechanism returns The PR's tests likely passed, as v5 was running them using TypeScript 2.0 and the change regarding the extension of built-ins was introduced in 2.1. Here's a playground link if you want to try it out. |
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.
Overall it looks good. I'm confused about the typings changes, we're building against TS 2.8, not 2.4 so I"m surprised there were any issues with the areas touched... We should keep those type changes to a separate PR.
observable.first().subscribe(); | ||
} catch (err) { | ||
expect(err instanceof EmptyError).to.equal(true); | ||
expect(err.stack).to.not.be.undefined; |
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.be.a('string')
would be a more specific check here.
@@ -33,6 +33,7 @@ describe('Observable.prototype.timeout', () => { | |||
throw new Error('this should not next'); | |||
}, err => { | |||
expect(err).to.be.an.instanceof(Rx.TimeoutError); | |||
expect(err.stack).to.not.be.undefined; |
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.
Same as the other spot. .to.be.a('string')
@@ -262,6 +262,7 @@ describe('Observable.ajax', () => { | |||
}); | |||
|
|||
expect(error instanceof Rx.AjaxError).to.be.true; | |||
expect(error.stack).to.not.be.undefined; |
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.
Same .to.be.a('string')
@@ -91,7 +91,7 @@ export class FromObservable<T> extends Observable<T> { | |||
return new FromObservable<T>(ish, scheduler); | |||
} else if (isArray(ish)) { | |||
return new ArrayObservable<T>(ish, scheduler); | |||
} else if (isPromise(ish)) { | |||
} else if (isPromise<T>(ish)) { |
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 change seems unrelated.
@@ -66,7 +66,7 @@ import { subscribeToResult } from '../util/subscribeToResult'; | |||
*/ | |||
export function _catch<T, R>(this: Observable<T>, selector: (err: any, caught: Observable<T>) => ObservableInput<R>): Observable<T | R> { | |||
const operator = new CatchOperator(selector); | |||
const caught = this.lift(operator); | |||
const caught = this.lift<T>(operator); |
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 change seems unrelated.
constructor() { | ||
const err: any = super('Timeout has occurred'); | ||
this.err = err; | ||
(<any> this).name = err.name = 'TimeoutError'; |
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 as any
.. we're trying to move away from this style of casing and move to as whatever
@benlesh I'm suggesting that this PR should be closed - i.e. not merged - as the current v6 implementation is better behaved. Do you not agree? |
Does the new implementation “touch” the stack? Eg access the getter/setter of the stack property? Because that was the actual problem I described. If it uses inheritance and does not access that field ahead of time then we should be fine without this PR and I would also suggest closing it. |
@hermanbanken I don't believe it does, I did some checking of the |
@hermanbanken Just did a quick perf test and can confirm that the current implementations - that call |
@cartant ... sorry, I missed your comment. Looking at this again, I believe you're right. Sorry, @hermanbanken. I really appreciate your effort here, but we have to close this one. I hope this doesn't put you off from further contributions. I really do appreciate the effort here! ❤️ |
Description:
The
Observable.timeout
method creates a TimeoutError on usage, before actually needing it. While this correctly sets the timeout's stacktrace to the use, greatly simplifying finding bugs, it also generates the stacktrace - with the current implementation - due to.stack
being accessed.Especially when using compiled languages like TypeScript with the source-map-support library this would cause expensive source map parsing (all while no error occurred yet). See for example this issue with ts-jest for more details.
This PR changes Rx' custom errors to access Error.stack lazily.
Another solution would be to just skip the
this.stack
assignment and rely on the proper inheritance of Error, not sure if that would work.Related issue (if exists):