Skip to content

Commit 56a7591

Browse files
committed
fix: make error handling more consistent
The run and stream methods on both the Connection and Query classes handled errors inconsistently, sometimes throwing errors synchronously and sometimes returning rejected promises or observables. This commit aims to make all of them consistent by always returning a promise or observable. BREAKING CHANGE: The run and stream methods of the Connection and Query classes no longer throw exceptions. Instead they return a rejected promise or an observable that will immediately error.
1 parent 8fe9218 commit 56a7591

File tree

4 files changed

+110
-63
lines changed

4 files changed

+110
-63
lines changed

src/connection.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,16 @@ export class Connection extends Builder<Query> {
208208
* @returns {Promise<Dictionary<R>[]>}
209209
*/
210210
run<R = any>(query: Query): Promise<Dictionary<R>[]> {
211+
if (!this.open) {
212+
return AnyPromise.reject(
213+
new Error('Cannot run query; connection is not open.'),
214+
) as Promise<Dictionary<R>[]>;
215+
}
216+
211217
if (query.getClauses().length === 0) {
212-
throw Error('Cannot run query: no clauses attached to the query.');
218+
return AnyPromise.reject(
219+
new Error('Cannot run query: no clauses attached to the query.'),
220+
) as Promise<Dictionary<R>[]>;
213221
}
214222

215223
const session = this.session();
@@ -229,7 +237,7 @@ export class Connection extends Builder<Query> {
229237
.catch((error) => {
230238
session.close();
231239
return Promise.reject(error);
232-
}) as any;
240+
}) as Promise<Dictionary<R>[]>;
233241
}
234242

235243
/**
@@ -300,25 +308,27 @@ export class Connection extends Builder<Query> {
300308
* In practice this should never happen unless you're doing some strange things.
301309
*/
302310
stream<R = any>(query: Query): Observable<Dictionary<R>> {
303-
if (!this.open) {
304-
throw Error('Cannot run query; connection is not open.');
305-
}
311+
return new Observable((subscriber: Observer<Dictionary<R>>): void => {
312+
if (!this.open) {
313+
subscriber.error(new Error('Cannot run query; connection is not open.'));
314+
return;
315+
}
306316

307-
if (query.getClauses().length === 0) {
308-
throw Error('Cannot run query: no clauses attached to the query.');
309-
}
317+
if (query.getClauses().length === 0) {
318+
subscriber.error(Error('Cannot run query: no clauses attached to the query.'));
319+
return;
320+
}
310321

311-
const session = this.session();
312-
if (!session) {
313-
throw Error('Cannot run query: connection is not open.');
314-
}
322+
const session = this.session();
323+
if (!session) {
324+
throw Error('Cannot run query: connection is not open.');
325+
}
315326

316-
// Run the query
317-
const queryObj = query.buildQueryObject();
318-
const result = session.run(queryObj.query, queryObj.params);
327+
// Run the query
328+
const queryObj = query.buildQueryObject();
329+
const result = session.run(queryObj.query, queryObj.params);
319330

320-
// Subscribe to the result and clean up the session
321-
return new Observable((subscriber: Observer<Dictionary<R>>): void => {
331+
// Subscribe to the result and clean up the session
322332
// Note: Neo4j observables use a different subscribe syntax to RxJS observables
323333
result.subscribe({
324334
onNext: (record) => {

src/query.spec.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Query } from './query';
2-
import { expect } from '../test-setup';
1+
// tslint:disable-next-line import-name
2+
import Observable from 'any-observable';
33
import { Dictionary, each } from 'lodash';
4-
import { node, NodePattern } from './clauses';
5-
import { mockConnection } from '../tests/connection.mock';
64
import { spy, stub } from 'sinon';
5+
import { expect } from '../test-setup';
6+
import { mockConnection } from '../tests/connection.mock';
77
import { ClauseCollection } from './clause-collection';
8+
import { node, NodePattern } from './clauses';
9+
import { Query } from './query';
810

911
describe('Query', () => {
1012
describe('query methods', () => {
@@ -96,9 +98,17 @@ describe('Query', () => {
9698
});
9799

98100
describe('#stream', () => {
99-
it('should throw if there is no attached connection object', () => {
100-
const query = new Query();
101-
expect(() => query.stream()).to.throw(Error, 'no connection object available');
101+
it('should return an errored observable if there is no attached connection object', () => {
102+
const observable = new Query().stream();
103+
expect(observable).to.be.an.instanceOf(Observable);
104+
observable.subscribe({
105+
next: () => expect.fail(null, null, 'Observable should not emit anything'),
106+
error(error) {
107+
expect(error).to.be.instanceOf(Error);
108+
expect(error.message).to.include('no connection object available');
109+
},
110+
complete: () => expect.fail(null, null, 'Observable should not complete successfully'),
111+
});
102112
});
103113

104114
it('should run the query on its connection', () => {

src/query.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// tslint:disable-next-line import-name
22
import AnyPromise from 'any-promise';
3+
// tslint:disable-next-line import-name
4+
import Observable from 'any-observable';
35
import { Observable as RxObservable } from 'rxjs';
46
import { Dictionary } from 'lodash';
5-
import { Connection } from './connection';
7+
import { Connection, Observer } from './connection';
68
import { Builder } from './builder';
79
import { ClauseCollection } from './clause-collection';
810
import { Clause, QueryObject } from './clause';
@@ -75,16 +77,12 @@ export class Query extends Builder<Query> {
7577
*/
7678
run<R = any>(): Promise<Dictionary<R>[]> {
7779
if (!this.connection) {
78-
return AnyPromise.reject(Error('Cannot run query; no connection object available.')) as any;
80+
return AnyPromise.reject(
81+
new Error('Cannot run query; no connection object available.'),
82+
) as Promise<Dictionary<R>[]>;
7983
}
8084

81-
// connection.run can throw errors synchronously. This is highly inconsistent and will be
82-
// fixed in the future, but for now we need to catch synchronous errors and reject them.
83-
try {
84-
return this.connection.run<R>(this);
85-
} catch (error) {
86-
return AnyPromise.reject(error) as any;
87-
}
85+
return this.connection.run<R>(this);
8886
}
8987

9088
/**
@@ -135,7 +133,9 @@ export class Query extends Builder<Query> {
135133
*/
136134
stream<R = any>(): RxObservable<Dictionary<R>> {
137135
if (!this.connection) {
138-
throw Error('Cannot run query; no connection object available.');
136+
return new Observable((subscriber: Observer<Dictionary<R>>): void => {
137+
subscriber.error(new Error('Cannot run query; no connection object available.'));
138+
});
139139
}
140140

141141
return this.connection.stream<R>(this);

tests/connection.test.ts

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import Observable from 'any-observable';
33
import { Dictionary, each } from 'lodash';
44
import { tap } from 'rxjs/operators';
5-
import { SinonSpy, spy } from 'sinon';
65
import { v1 as neo4j } from 'neo4j-driver';
7-
import { AuthToken, Config } from 'neo4j-driver/types/v1/driver';
86
import { Driver } from 'neo4j-driver/types/v1';
7+
import { AuthToken, Config } from 'neo4j-driver/types/v1/driver';
8+
import { SinonSpy, spy } from 'sinon';
99
import { Connection, Node, Query } from '../src';
1010
import { NodePattern } from '../src/clauses';
1111
import { expect } from '../test-setup';
@@ -108,15 +108,15 @@ describe('Connection', () => {
108108
});
109109

110110
describe('#run', () => {
111-
it('should throw if there are no clauses in the query', () => {
112-
const run = () => connection.run(connection.query());
113-
expect(run).to.throw(Error, 'no clauses');
111+
it('should reject if there are no clauses in the query', () => {
112+
const promise = connection.run(connection.query());
113+
expect(promise).to.be.rejectedWith(Error, 'no clauses');
114114
});
115115

116-
it('should throw if the connection has been closed', () => {
116+
it('should reject if the connection has been closed', () => {
117117
connection.close();
118-
const run = () => connection.run(connection.query().matchNode('node'));
119-
expect(run).to.throw(Error, 'connection is not open');
118+
const promise = connection.run(connection.query().matchNode('node'));
119+
expect(promise).to.be.rejectedWith(Error, 'connection is not open');
120120
});
121121

122122
it('should run the query through a session', () => {
@@ -171,15 +171,33 @@ describe('Connection', () => {
171171
connection.close();
172172
});
173173

174-
it('should throw if there are no clauses in the query', () => {
175-
const stream = () => connection.stream(connection.query());
176-
expect(stream).to.throw(Error, 'no clauses');
174+
it('should return errored observable if there are no clauses in the query', () => {
175+
const observable = connection.stream(connection.query());
176+
expect(observable).to.be.an.instanceOf(Observable);
177+
178+
observable.subscribe({
179+
next: () => expect.fail(null, null, 'Observable should not emit anything'),
180+
error(error) {
181+
expect(error).to.be.instanceOf(Error);
182+
expect(error.message).to.include('no clauses');
183+
},
184+
complete: () => expect.fail(null, null, 'Observable should not complete successfully'),
185+
});
177186
});
178187

179-
it('should throw if the connection has been closed', () => {
188+
it('should return errored observable if the connection has been closed', () => {
180189
connection.close();
181-
const stream = () => connection.stream(query);
182-
expect(stream).to.throw(Error, 'connection is not open');
190+
const observable = connection.stream(query);
191+
expect(observable).to.be.an.instanceOf(Observable);
192+
193+
observable.subscribe({
194+
next: () => expect.fail(null, null, 'Observable should not emit anything'),
195+
error(error) {
196+
expect(error).to.be.instanceOf(Error);
197+
expect(error.message).to.include('connection is not open');
198+
},
199+
complete: () => expect.fail(null, null, 'Observable should not complete successfully'),
200+
});
183201
});
184202

185203
it('should run the query through a session', () => {
@@ -216,7 +234,7 @@ describe('Connection', () => {
216234
expect(observable).to.be.an.instanceOf(Observable);
217235
observable.subscribe({
218236
next: () => expect.fail(null, null, 'Observable should not emit any items'),
219-
error: () => {
237+
error() {
220238
expect(sessionCloseSpy.calledOnce);
221239
done();
222240
},
@@ -227,32 +245,41 @@ describe('Connection', () => {
227245

228246
describe('query methods', () => {
229247
const methods: Dictionary<Function> = {
230-
query: () => connection.query(),
231-
matchNode: () => connection.matchNode('Node'),
232-
match: () => connection.match(new NodePattern('Node')),
233-
optionalMatch: () => connection.optionalMatch(new NodePattern('Node')),
234248
create: () => connection.create(new NodePattern('Node')),
235-
createUnique: () => connection.createUnique(new NodePattern('Node')),
236249
createNode: () => connection.createNode('Node'),
250+
createUnique: () => connection.createUnique(new NodePattern('Node')),
237251
createUniqueNode: () => connection.createUniqueNode('Node'),
238-
return: () => connection.return('node'),
239-
returnDistinct: () => connection.returnDistinct('node'),
252+
delete: () => connection.delete('node'),
253+
detachDelete: () => connection.detachDelete('node'),
254+
limit: () => connection.limit(1),
255+
match: () => connection.match(new NodePattern('Node')),
256+
matchNode: () => connection.matchNode('Node'),
257+
merge: () => connection.merge(new NodePattern('Node')),
258+
onCreateSet: () => connection.onCreate.set({}, { merge: false }),
259+
onCreateSetLabels: () => connection.onCreate.setLabels({}),
260+
onCreateSetValues: () => connection.onCreate.setValues({}),
261+
onCreateSetVariables: () => connection.onCreate.setVariables({}, false),
262+
onMatchSet: () => connection.onMatch.set({}, { merge: false }),
263+
onMatchSetLabels: () => connection.onMatch.setLabels({}),
264+
onMatchSetValues: () => connection.onMatch.setValues({}),
265+
onMatchSetVariables: () => connection.onMatch.setVariables({}, false),
266+
optionalMatch: () => connection.optionalMatch(new NodePattern('Node')),
267+
orderBy: () => connection.orderBy('name'),
268+
query: () => connection.query(),
269+
raw: () => connection.raw('name'),
240270
remove: () => connection.remove({ properties: { node: ['prop1', 'prop2'] } }),
241271
removeProperties: () => connection.removeProperties({ node: ['prop1', 'prop2'] }),
242272
removeLabels: () => connection.removeLabels({ node: 'label' }),
243-
with: () => connection.with('node'),
244-
unwind: () => connection.unwind([1, 2, 3], 'number'),
245-
delete: () => connection.delete('node'),
246-
detachDelete: () => connection.detachDelete('node'),
273+
return: () => connection.return('node'),
274+
returnDistinct: () => connection.returnDistinct('node'),
247275
set: () => connection.set({}, { merge: false }),
248276
setLabels: () => connection.setLabels({}),
249277
setValues: () => connection.setValues({}),
250278
setVariables: () => connection.setVariables({}, false),
251279
skip: () => connection.skip(1),
252-
limit: () => connection.limit(1),
280+
unwind: () => connection.unwind([1, 2, 3], 'number'),
253281
where: () => connection.where([]),
254-
orderBy: () => connection.orderBy('name'),
255-
raw: () => connection.raw('name'),
282+
with: () => connection.with('node'),
256283
};
257284

258285
each(methods, (fn, name) => {

0 commit comments

Comments
 (0)