Skip to content

Commit e4bed2a

Browse files
authored
fix: finalize behaves well with useDeprecatedSynchronousErrorHandling (#6251)
* fix: finalize behaves well with useDeprecatedSynchronousErrorHandling Adds tests and ensures a few more scenarios that were hit in Google because they use the deprecated synchronous error handling. fixes #6250 * refactor: Move deprecated junk to its own method Just for readability. The deprecated stuff is a hot mess, and this shows what we get to delete in version 8 more cleanly. * refactor: Add more comments * test: Add more tests around gross mode and finalize
1 parent 23bc7fd commit e4bed2a

File tree

3 files changed

+192
-33
lines changed

3 files changed

+192
-33
lines changed

spec/Observable-spec.ts

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from 'chai';
22
import * as sinon from 'sinon';
33
import { Observer, TeardownLogic } from '../src/internal/types';
44
import { Observable, config, Subscription, noop, Subscriber, Operator, NEVER, Subject, of, throwError, empty } from 'rxjs';
5-
import { map, multicast, refCount, filter, count, tap, combineLatest, concat, merge, race, zip, catchError, concatMap, switchMap, publish, publishLast, publishBehavior, share} from 'rxjs/operators';
5+
import { map, multicast, refCount, filter, count, tap, combineLatest, concat, merge, race, zip, catchError, concatMap, switchMap, publish, publishLast, publishBehavior, share, finalize} from 'rxjs/operators';
66
import { TestScheduler } from 'rxjs/testing';
77
import { observableMatcher } from './helpers/observableMatcher';
88

@@ -633,7 +633,137 @@ describe('Observable', () => {
633633
.pipe(switchMap(() => throwError(new Error('Avast! Thar be a new error!'))))
634634
.subscribe(console.log);
635635
}).to.throw('Avast! Thar be a new error!');
636-
})
636+
});
637+
638+
it('should teardown even with a synchronous error', () => {
639+
let called = false;
640+
const badObservable = new Observable((subscriber) => {
641+
subscriber.add(() => {
642+
called = true;
643+
});
644+
645+
subscriber.error(new Error('bad'));
646+
});
647+
648+
try {
649+
badObservable.subscribe();
650+
} catch (err) {
651+
// do nothing
652+
}
653+
expect(called).to.be.true;
654+
});
655+
656+
it('should teardown even with a synchronous thrown error', () => {
657+
let called = false;
658+
const badObservable = new Observable((subscriber) => {
659+
subscriber.add(() => {
660+
called = true;
661+
});
662+
663+
throw new Error('bad');
664+
});
665+
666+
try {
667+
badObservable.subscribe();
668+
} catch (err) {
669+
// do nothing
670+
}
671+
expect(called).to.be.true;
672+
});
673+
674+
675+
it('should handle empty string sync errors', () => {
676+
const badObservable = new Observable(() => {
677+
throw '';
678+
});
679+
680+
let caught = false;
681+
try {
682+
badObservable.subscribe();
683+
} catch (err) {
684+
caught = true;
685+
expect(err).to.equal('');
686+
}
687+
expect(caught).to.be.true;
688+
});
689+
690+
it('should execute finalize even with a sync error', () => {
691+
let called = false;
692+
const badObservable = new Observable((subscriber) => {
693+
subscriber.error(new Error('bad'));
694+
}).pipe(
695+
finalize(() => {
696+
called = true;
697+
})
698+
);
699+
700+
try {
701+
badObservable.subscribe();
702+
} catch (err) {
703+
// do nothing
704+
}
705+
expect(called).to.be.true;
706+
});
707+
708+
it('should execute finalize even with a sync thrown error', () => {
709+
let called = false;
710+
const badObservable = new Observable(() => {
711+
throw new Error('bad');
712+
}).pipe(
713+
finalize(() => {
714+
called = true;
715+
})
716+
);
717+
718+
try {
719+
badObservable.subscribe();
720+
} catch (err) {
721+
// do nothing
722+
}
723+
expect(called).to.be.true;
724+
});
725+
726+
it('should execute finalize in order even with a sync error', () => {
727+
const results: any[] = [];
728+
const badObservable = new Observable((subscriber) => {
729+
subscriber.error(new Error('bad'));
730+
}).pipe(
731+
finalize(() => {
732+
results.push(1);
733+
}),
734+
finalize(() => {
735+
results.push(2)
736+
})
737+
);
738+
739+
try {
740+
badObservable.subscribe();
741+
} catch (err) {
742+
// do nothing
743+
}
744+
expect(results).to.deep.equal([1, 2]);
745+
});
746+
747+
it('should execute finalize in order even with a sync thrown error', () => {
748+
const results: any[] = [];
749+
const badObservable = new Observable(() => {
750+
throw new Error('bad');
751+
}).pipe(
752+
finalize(() => {
753+
results.push(1);
754+
}),
755+
finalize(() => {
756+
results.push(2)
757+
})
758+
);
759+
760+
try {
761+
badObservable.subscribe();
762+
} catch (err) {
763+
// do nothing
764+
}
765+
expect(results).to.deep.equal([1, 2]);
766+
});
637767

638768
afterEach(() => {
639769
config.useDeprecatedSynchronousErrorHandling = false;

src/internal/Observable.ts

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -215,43 +215,67 @@ export class Observable<T> implements Subscribable<T> {
215215
): Subscription {
216216
const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);
217217

218-
// If we have an operator, it's the result of a lift, and we let the lift
219-
// mechanism do the subscription for us in the operator call. Otherwise,
220-
// if we have a source, it's a trusted observable we own, and we can call
221-
// the `_subscribe` without wrapping it in a try/catch. If we are supposed to
222-
// use the deprecated sync error handling, then we don't need the try/catch either
223-
// otherwise, it may be from a user-made observable instance, and we want to
224-
// wrap it in a try/catch so we can handle errors appropriately.
225-
const { operator, source } = this;
226-
227-
let dest: any = subscriber;
228218
if (config.useDeprecatedSynchronousErrorHandling) {
229-
dest._syncErrorHack_isSubscribing = true;
219+
this._deprecatedSyncErrorSubscribe(subscriber);
220+
} else {
221+
const { operator, source } = this;
222+
subscriber.add(
223+
operator
224+
? // We're dealing with a subscription in the
225+
// operator chain to one of our lifted operators.
226+
operator.call(subscriber, source)
227+
: source
228+
? // If `source` has a value, but `operator` does not, something that
229+
// had intimate knowledge of our API, like our `Subject`, must have
230+
// set it. We're going to just call `_subscribe` directly.
231+
this._subscribe(subscriber)
232+
: // In all other cases, we're likely wrapping a user-provided initializer
233+
// function, so we need to catch errors and handle them appropriately.
234+
this._trySubscribe(subscriber)
235+
);
230236
}
237+
return subscriber;
238+
}
231239

232-
subscriber.add(
233-
operator
234-
? operator.call(subscriber, source)
235-
: source || config.useDeprecatedSynchronousErrorHandling
236-
? this._subscribe(subscriber)
237-
: this._trySubscribe(subscriber)
238-
);
240+
/**
241+
* REMOVE THIS ENTIRE METHOD IN VERSION 8.
242+
*/
243+
private _deprecatedSyncErrorSubscribe(subscriber: Subscriber<unknown>) {
244+
let dest: any = subscriber;
245+
dest._syncErrorHack_isSubscribing = true;
246+
const { operator } = this;
247+
if (operator) {
248+
// We don't need to try/catch on operators, as they
249+
// are doing their own try/catching, and will
250+
// properly decorate the subscriber with `__syncError`.
251+
subscriber.add(operator.call(subscriber, this.source));
252+
} else {
253+
try {
254+
this._subscribe(subscriber);
255+
} catch (err) {
256+
dest.__syncError = err;
257+
}
258+
}
239259

240-
if (config.useDeprecatedSynchronousErrorHandling) {
241-
dest._syncErrorHack_isSubscribing = false;
242-
// In the case of the deprecated sync error handling,
243-
// we need to crawl forward through our subscriber chain and
244-
// look to see if there's any synchronously thrown errors.
245-
// Does this suck for perf? Yes. So stop using the deprecated sync
246-
// error handling already. We're removing this in v8.
247-
while (dest) {
248-
if (dest.__syncError) {
260+
// In the case of the deprecated sync error handling,
261+
// we need to crawl forward through our subscriber chain and
262+
// look to see if there's any synchronously thrown errors.
263+
// Does this suck for perf? Yes. So stop using the deprecated sync
264+
// error handling already. We're removing this in v8.
265+
while (dest) {
266+
// Technically, someone could throw something falsy, like 0, or "",
267+
// so we need to check to see if anything was thrown, and we know
268+
// that by the mere existence of `__syncError`.
269+
if ('__syncError' in dest) {
270+
try {
249271
throw dest.__syncError;
272+
} finally {
273+
subscriber.unsubscribe();
250274
}
251-
dest = dest.destination;
252275
}
276+
dest = dest.destination;
253277
}
254-
return subscriber;
278+
dest._syncErrorHack_isSubscribing = false;
255279
}
256280

257281
/** @internal */

src/internal/operators/finalize.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ import { operate } from '../util/lift';
5858
*/
5959
export function finalize<T>(callback: () => void): MonoTypeOperatorFunction<T> {
6060
return operate((source, subscriber) => {
61-
source.subscribe(subscriber);
62-
subscriber.add(callback);
61+
// TODO: This try/finally was only added for `useDeprecatedSynchronousErrorHandling`.
62+
// REMOVE THIS WHEN THAT HOT GARBAGE IS REMOVED IN V8.
63+
try {
64+
source.subscribe(subscriber);
65+
} finally {
66+
subscriber.add(callback);
67+
}
6368
});
6469
}

0 commit comments

Comments
 (0)