Skip to content

Commit cd9626a

Browse files
authored
feat(Observable): unhandled errors are now reported to HostReportErrors (#3062)
BREAKING CHANGE: Unhandled errors are no longer caught and rethrown, rather they are caught and scheduled to be thrown, which causes them to be reported to window.onerror or process.on('error'), depending on the environment. Consequently, teardown after a synchronous, unhandled, error will no longer occur, as the teardown would not exist, and producer interference cannot occur
1 parent ad143f1 commit cd9626a

15 files changed

+217
-506
lines changed

spec/Observable-spec.ts

Lines changed: 117 additions & 205 deletions
Large diffs are not rendered by default.

spec/Subscriber-spec.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,10 @@
11
import { expect } from 'chai';
2-
import * as sinon from 'sinon';
32
import * as Rx from '../src/Rx';
43

54
const Subscriber = Rx.Subscriber;
65

76
/** @test {Subscriber} */
87
describe('Subscriber', () => {
9-
describe('when created through create()', () => {
10-
it('should not call error() if next() handler throws an error', () => {
11-
const errorSpy = sinon.spy();
12-
const completeSpy = sinon.spy();
13-
14-
const subscriber = Subscriber.create(
15-
(value: any) => {
16-
if (value === 2) {
17-
throw 'error!';
18-
}
19-
},
20-
errorSpy,
21-
completeSpy
22-
);
23-
24-
subscriber.next(1);
25-
expect(() => {
26-
subscriber.next(2);
27-
}).to.throw('error!');
28-
29-
expect(errorSpy).not.have.been.called;
30-
expect(completeSpy).not.have.been.called;
31-
});
32-
});
33-
348
it('should ignore next messages after unsubscription', () => {
359
let times = 0;
3610

spec/observables/dom/ajax-spec.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('Observable.ajax', () => {
5757
expect(axObjectStub).to.have.been.called;
5858
});
5959

60-
it('should throw if not able to create XMLHttpRequest', () => {
60+
it('should raise an error if not able to create XMLHttpRequest', () => {
6161
root.XMLHttpRequest = null;
6262
root.ActiveXObject = null;
6363

@@ -66,9 +66,7 @@ describe('Observable.ajax', () => {
6666
method: ''
6767
};
6868

69-
expect(() => {
70-
Rx.Observable.ajax(obj).subscribe();
71-
}).to.throw();
69+
Rx.Observable.ajax(obj).subscribe(null, err => expect(err).to.exist);
7270
});
7371

7472
it('should create XMLHttpRequest for CORS', () => {
@@ -100,7 +98,7 @@ describe('Observable.ajax', () => {
10098
expect(xDomainStub).to.have.been.called;
10199
});
102100

103-
it('should throw if not able to create CORS request', () => {
101+
it('should raise an error if not able to create CORS request', () => {
104102
root.XMLHttpRequest = null;
105103
root.XDomainRequest = null;
106104

@@ -111,9 +109,7 @@ describe('Observable.ajax', () => {
111109
withCredentials: true
112110
};
113111

114-
expect(() => {
115-
Rx.Observable.ajax(obj).subscribe();
116-
}).to.throw();
112+
Rx.Observable.ajax(obj).subscribe(null, err => expect(err).to.exist);
117113
});
118114

119115
it('should set headers', () => {
@@ -805,7 +801,7 @@ describe('Observable.ajax', () => {
805801

806802
it('should work fine when XMLHttpRequest ontimeout property is monkey patched', function() {
807803
Object.defineProperty(root.XMLHttpRequest.prototype, 'ontimeout', {
808-
set: function (fn: (e: ProgressEvent) => any) {
804+
set(fn: (e: ProgressEvent) => any) {
809805
const wrapFn = (ev: ProgressEvent) => {
810806
const result = fn.call(this, ev);
811807
if (result === false) {
@@ -825,7 +821,11 @@ describe('Observable.ajax', () => {
825821
};
826822

827823
Rx.Observable.ajax(ajaxRequest)
828-
.subscribe();
824+
.subscribe({
825+
error(err) {
826+
/* expected, ignore */
827+
}
828+
});
829829

830830
const request = MockXMLHttpRequest.mostRecent;
831831
try {
@@ -881,7 +881,7 @@ describe('Observable.ajax', () => {
881881

882882
it('should work fine when XMLHttpRequest onerror property is monkey patched', function() {
883883
Object.defineProperty(root.XMLHttpRequest.prototype, 'onerror', {
884-
set: function (fn: (e: ProgressEvent) => any) {
884+
set(fn: (e: ProgressEvent) => any) {
885885
const wrapFn = (ev: ProgressEvent) => {
886886
const result = fn.call(this, ev);
887887
if (result === false) {
@@ -899,7 +899,11 @@ describe('Observable.ajax', () => {
899899
Rx.Observable.ajax({
900900
url: '/flibbertyJibbet'
901901
})
902-
.subscribe();
902+
.subscribe({
903+
error(err) {
904+
/* expected */
905+
}
906+
});
903907

904908
const request = MockXMLHttpRequest.mostRecent;
905909

spec/observables/from-promise-spec.ts

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -155,56 +155,4 @@ describe('Observable.fromPromise', () => {
155155
done();
156156
});
157157
});
158-
159-
if (typeof process === 'object' && Object.prototype.toString.call(process) === '[object process]') {
160-
it('should globally throw unhandled errors on process', (done: MochaDone) => {
161-
const originalException = process.listeners('uncaughtException');
162-
process.removeAllListeners('uncaughtException');
163-
process.once('uncaughtException', function (error) {
164-
expect(error).to.be.an('error', 'fail');
165-
originalException.forEach(l => process.addListener('uncaughtException', l));
166-
done();
167-
});
168-
169-
Observable.fromPromise(Promise.reject('bad'))
170-
.subscribe(
171-
(x: any) => { done(new Error('should not be called')); },
172-
(e: any) => {
173-
expect(e).to.equal('bad');
174-
throw new Error('fail');
175-
}, () => {
176-
done(new Error('should not be called'));
177-
});
178-
});
179-
} else if (typeof window === 'object' &&
180-
(Object.prototype.toString.call(window) === '[object global]' || Object.prototype.toString.call(window) === '[object Window]')) {
181-
it('should globally throw unhandled errors on window', (done: MochaDone) => {
182-
const expected = ['Uncaught fail', 'fail', 'Script error.', 'uncaught exception: fail'];
183-
const current = window.onerror;
184-
window.onerror = null;
185-
186-
let invoked = false;
187-
function onException(e) {
188-
if (invoked) {
189-
return;
190-
}
191-
invoked = true;
192-
expect(expected).to.contain(e);
193-
window.onerror = current;
194-
done();
195-
}
196-
197-
window.onerror = onException;
198-
199-
Observable.fromPromise(Promise.reject('bad'))
200-
.subscribe(
201-
(x: any) => { done(new Error('should not be called')); },
202-
(e: any) => {
203-
expect(e).to.equal('bad');
204-
throw 'fail';
205-
}, () => {
206-
done(new Error('should not be called'));
207-
});
208-
});
209-
}
210-
});
158+
});

spec/observables/fromEvent-spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ describe('Observable.fromEvent', () => {
125125
}
126126
};
127127

128-
const subscribe = () => Observable.fromEvent(<any>obj, 'click').subscribe();
129-
expect(subscribe).to.throw(TypeError, 'Invalid event target');
128+
Observable.fromEvent(obj as any, 'click').subscribe({
129+
error(err) { expect(err).to.deep.equal(new TypeError('Invalid event target')); }
130+
});
130131
});
131132

132133
it('should pass through options to addEventListener', () => {

spec/operators/catch-spec.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -297,44 +297,6 @@ describe('Observable.prototype.catch', () => {
297297
sandbox.restore();
298298
});
299299

300-
it('should chain a throw from a promise using throw', (done: MochaDone) => {
301-
const subscribeSpy = sinon.spy();
302-
const testError = new Error('BROKEN PROMISE');
303-
Observable.fromPromise(Promise.reject(testError)).catch(err => {
304-
throw new Error('BROKEN THROW');
305-
}).subscribe(subscribeSpy);
306-
307-
trueSetTimeout(() => {
308-
try {
309-
timers.tick(1);
310-
} catch (e) {
311-
expect(subscribeSpy).not.to.be.called;
312-
expect(e.message).to.equal('BROKEN THROW');
313-
return done();
314-
}
315-
done(new Error('This should have thrown an error'));
316-
}, 0);
317-
});
318-
319-
it('should chain a throw from a promise using Observable.throw', (done: MochaDone) => {
320-
const subscribeSpy = sinon.spy();
321-
const testError = new Error('BROKEN PROMISE');
322-
Observable.fromPromise(Promise.reject(testError)).catch(err =>
323-
Observable.throw(new Error('BROKEN THROW'))
324-
).subscribe(subscribeSpy);
325-
326-
trueSetTimeout(() => {
327-
try {
328-
timers.tick(1);
329-
} catch (e) {
330-
expect(subscribeSpy).not.to.be.called;
331-
expect(e.message).to.equal('BROKEN THROW');
332-
return done();
333-
}
334-
done(new Error('This should have thrown an error'));
335-
}, 0);
336-
});
337-
338300
it('should chain a throw from a promise using Observable.throw', (done: MochaDone) => {
339301
const subscribeSpy = sinon.spy();
340302
const errorSpy = sinon.spy();

spec/operators/do-spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const Subject = Rx.Subject;
1313

1414
/** @test {do} */
1515
describe('Observable.prototype.do', () => {
16-
asDiagram('do(x => console.log(x))')('should mirror multiple values and complete', () => {
16+
asDiagram('do(x => console.log(x))')
17+
('should mirror multiple values and complete', () => {
1718
const e1 = cold('--1--2--3--|');
1819
const e1subs = '^ !';
1920
const expected = '--1--2--3--|';
@@ -68,8 +69,8 @@ describe('Observable.prototype.do', () => {
6869

6970
it('should handle everything with a Subject', (done: MochaDone) => {
7071
const expected = [1, 2, 3];
71-
const results = [];
72-
const subject = new Subject();
72+
const results: number[] = [];
73+
const subject = new Subject<number>();
7374

7475
subject.subscribe({
7576
next: (x: any) => {
@@ -85,7 +86,7 @@ describe('Observable.prototype.do', () => {
8586
});
8687

8788
Observable.of(1, 2, 3)
88-
.do(<Rx.Observer<number>>subject)
89+
.do(subject)
8990
.subscribe();
9091
});
9192

spec/operators/map-spec.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const Observable = Rx.Observable;
1313
// function shortcuts
1414
const addDrama = function (x) { return x + '!'; };
1515
const identity = function (x) { return x; };
16-
const throwError = function () { throw new Error(); };
1716

1817
/** @test {map} */
1918
describe('Observable.prototype.map', () => {
@@ -89,16 +88,6 @@ describe('Observable.prototype.map', () => {
8988
expectSubscriptions(a.subscriptions).toBe(asubs);
9089
});
9190

92-
it('should propagate errors from subscribe', () => {
93-
const r = () => {
94-
Observable.of(1)
95-
.map(identity)
96-
.subscribe(throwError);
97-
};
98-
99-
expect(r).to.throw();
100-
});
101-
10291
it('should not map an empty observable', () => {
10392
const a = cold('|');
10493
const asubs = '(^!)';
@@ -187,19 +176,14 @@ describe('Observable.prototype.map', () => {
187176
const expected = '--a--b---c----d--|';
188177
const values = {a: 5, b: 14, c: 23, d: 32};
189178

190-
let invoked = 0;
191179
const foo = {
192180
value: 42
193181
};
194182
const r = a
195183
.map(function (x: string, index: number) {
196-
invoked++;
197184
expect(this).to.equal(foo);
198185
return (parseInt(x) + 1) + (index * 10);
199-
}, foo)
200-
.do(null, null, () => {
201-
expect(invoked).to.equal(4);
202-
});
186+
}, foo);
203187

204188
expectObservable(r).toBe(expected, values);
205189
expectSubscriptions(a.subscriptions).toBe(asubs);

spec/operators/mapTo-spec.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect } from 'chai';
1+
22
import * as Rx from '../../src/Rx';
33
import marbleTestingSignature = require('../helpers/marble-testing'); // tslint:disable-line:no-require-imports
44

@@ -10,9 +10,6 @@ declare const expectSubscriptions: typeof marbleTestingSignature.expectSubscript
1010

1111
const Observable = Rx.Observable;
1212

13-
// function shortcuts
14-
const throwError = function () { throw new Error(); };
15-
1613
/** @test {mapTo} */
1714
describe('Observable.prototype.mapTo', () => {
1815
asDiagram('mapTo(\'a\')')('should map multiple values', () => {
@@ -61,16 +58,6 @@ describe('Observable.prototype.mapTo', () => {
6158
expectSubscriptions(a.subscriptions).toBe(asubs);
6259
});
6360

64-
it('should propagate errors from subscribe', () => {
65-
const r = () => {
66-
Observable.of(1)
67-
.mapTo(-1)
68-
.subscribe(throwError);
69-
};
70-
71-
expect(r).to.throw();
72-
});
73-
7461
it('should not map an empty observable', () => {
7562
const a = cold('|');
7663
const asubs = '(^!)';

spec/util/subscribeToResult-spec.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { subscribeToResult } from '../../src/util/subscribeToResult';
44
import { OuterSubscriber } from '../../src/OuterSubscriber';
55
import { $$iterator } from '../../src/symbol/iterator';
66
import $$symbolObservable from 'symbol-observable';
7-
import { Observable } from '../../src/Observable';
8-
import { Subject } from '../../src/Subject';
97

108
describe('subscribeToResult', () => {
119
it('should synchronously complete when subscribe to scalarObservable', () => {
@@ -180,14 +178,4 @@ describe('subscribeToResult', () => {
180178

181179
subscribeToResult(subscriber, null);
182180
});
183-
184-
it('should not swallow exception in inner subscriber', () => {
185-
const source = new Subject();
186-
187-
source.mergeMapTo(Observable.of(1, 2, 3)).subscribe(() => {
188-
throw new Error('meh');
189-
});
190-
191-
expect(() => source.next()).to.throw();
192-
});
193-
});
181+
});

0 commit comments

Comments
 (0)