Skip to content

Commit 9b21458

Browse files
authored
feat(throwIfEmpty): adds throwIfEmpty operator (#3368)
* feat(throwIfEmpty): adds throwIfEmpty operator This is a new, simple, operator that will emit an error if the source observable completes without emitting a value. This primitive operator can be used to compose other operators such as `first` and `last`, and is a good compliment for `defaultIfEmpty`. * docs(throwIfEmpty): Fix minor typo in example
1 parent 2cebbcc commit 9b21458

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { expect } from 'chai';
2+
import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing';
3+
import { EMPTY, of } from '../../src';
4+
import { EmptyError } from '../../src/internal/util/EmptyError';
5+
import { throwIfEmpty } from '../../src/operators';
6+
7+
/** @test {timeout} */
8+
describe('throwIfEmpty', () => {
9+
describe('with errorFactory', () => {
10+
it('should throw if empty', () => {
11+
const error = new Error('So empty inside');
12+
let thrown: any;
13+
14+
EMPTY.pipe(
15+
throwIfEmpty(() => error),
16+
)
17+
.subscribe({
18+
error(err) {
19+
thrown = err;
20+
}
21+
});
22+
23+
expect(thrown).to.equal(error);
24+
});
25+
26+
it('should NOT throw if NOT empty', () => {
27+
const error = new Error('So empty inside');
28+
let thrown: any;
29+
30+
of('test').pipe(
31+
throwIfEmpty(() => error),
32+
)
33+
.subscribe({
34+
error(err) {
35+
thrown = err;
36+
}
37+
});
38+
39+
expect(thrown).to.be.undefined;
40+
});
41+
42+
it('should pass values through', () => {
43+
const source = cold('----a---b---c---|');
44+
const sub1 = '^ !';
45+
const expected = '----a---b---c---|';
46+
expectObservable(
47+
source.pipe(throwIfEmpty(() => new Error('test')))
48+
).toBe(expected);
49+
expectSubscriptions(source.subscriptions).toBe([sub1]);
50+
});
51+
52+
it('should never when never', () => {
53+
const source = cold('-');
54+
const sub1 = '^';
55+
const expected = '-';
56+
expectObservable(
57+
source.pipe(throwIfEmpty(() => new Error('test')))
58+
).toBe(expected);
59+
expectSubscriptions(source.subscriptions).toBe([sub1]);
60+
});
61+
62+
it('should error when empty', () => {
63+
const source = cold('----|');
64+
const sub1 = '^ !';
65+
const expected = '----#';
66+
expectObservable(
67+
source.pipe(throwIfEmpty(() => new Error('test')))
68+
).toBe(expected, undefined, new Error('test'));
69+
expectSubscriptions(source.subscriptions).toBe([sub1]);
70+
});
71+
});
72+
73+
describe('without errorFactory', () => {
74+
it('should throw EmptyError if empty', () => {
75+
let thrown: any;
76+
77+
EMPTY.pipe(
78+
throwIfEmpty(),
79+
)
80+
.subscribe({
81+
error(err) {
82+
thrown = err;
83+
}
84+
});
85+
86+
expect(thrown).to.be.instanceof(EmptyError);
87+
});
88+
89+
it('should NOT throw if NOT empty', () => {
90+
let thrown: any;
91+
92+
of('test').pipe(
93+
throwIfEmpty(),
94+
)
95+
.subscribe({
96+
error(err) {
97+
thrown = err;
98+
}
99+
});
100+
101+
expect(thrown).to.be.undefined;
102+
});
103+
104+
it('should pass values through', () => {
105+
const source = cold('----a---b---c---|');
106+
const sub1 = '^ !';
107+
const expected = '----a---b---c---|';
108+
expectObservable(
109+
source.pipe(throwIfEmpty())
110+
).toBe(expected);
111+
expectSubscriptions(source.subscriptions).toBe([sub1]);
112+
});
113+
114+
it('should never when never', () => {
115+
const source = cold('-');
116+
const sub1 = '^';
117+
const expected = '-';
118+
expectObservable(
119+
source.pipe(throwIfEmpty())
120+
).toBe(expected);
121+
expectSubscriptions(source.subscriptions).toBe([sub1]);
122+
});
123+
124+
it('should error when empty', () => {
125+
const source = cold('----|');
126+
const sub1 = '^ !';
127+
const expected = '----#';
128+
expectObservable(
129+
source.pipe(throwIfEmpty())
130+
).toBe(expected, undefined, new EmptyError());
131+
expectSubscriptions(source.subscriptions).toBe([sub1]);
132+
});
133+
});
134+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { tap } from './tap';
2+
import { EmptyError } from '../util/EmptyError';
3+
import { MonoTypeOperatorFunction } from '../types';
4+
5+
/**
6+
* If the source observable completes without emitting a value, it will emit
7+
* an error. The error will be created at that time by the optional
8+
* `errorFactory` argument, otherwise, the error will be {@link ErrorEmpty}.
9+
*
10+
* @example
11+
*
12+
* const click$ = fromEvent(button, 'click');
13+
*
14+
* clicks$.pipe(
15+
* takeUntil(timer(1000)),
16+
* throwIfEmpty(
17+
* () => new Error('the button was not clicked within 1 second')
18+
* ),
19+
* )
20+
* .subscribe({
21+
* next() { console.log('The button was clicked'); },
22+
* error(err) { console.error(err); },
23+
* });
24+
* @param {Function} [errorFactory] A factory function called to produce the
25+
* error to be thrown when the source observable completes without emitting a
26+
* value.
27+
*/
28+
export const throwIfEmpty =
29+
<T>(errorFactory: (() => any) = defaultErrorFactory) => tap<T>({
30+
hasValue: false,
31+
next() { this.hasValue = true; },
32+
complete() {
33+
if (!this.hasValue) {
34+
throw errorFactory();
35+
}
36+
}
37+
} as any);
38+
39+
function defaultErrorFactory() {
40+
return new EmptyError();
41+
}

src/operators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export { takeWhile } from '../internal/operators/takeWhile';
9090
export { tap } from '../internal/operators/tap';
9191
export { throttle } from '../internal/operators/throttle';
9292
export { throttleTime } from '../internal/operators/throttleTime';
93+
export { throwIfEmpty } from '../internal/operators/throwIfEmpty';
9394
export { timeInterval } from '../internal/operators/timeInterval';
9495
export { timeout } from '../internal/operators/timeout';
9596
export { timeoutWith } from '../internal/operators/timeoutWith';

0 commit comments

Comments
 (0)