Skip to content

Commit 9132939

Browse files
committed
Merge branch 'main' into NODE-4892-version-tag
2 parents 1cd6a31 + 5b837a9 commit 9132939

File tree

15 files changed

+216
-110
lines changed

15 files changed

+216
-110
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,29 @@ Deserialize stream data as BSON documents.
285285

286286
**Returns**: <code>Number</code> - returns the next index in the buffer after deserialization **x** numbers of documents.
287287

288+
## Error Handling
289+
290+
It is our recommendation to use `BSONError.isBSONError()` checks on errors and to avoid relying on parsing `error.message` and `error.name` strings in your code. We guarantee `BSONError.isBSONError()` checks will pass according to semver guidelines, but errors may be sub-classed or their messages may change at any time, even patch releases, as we see fit to increase the helpfulness of the errors.
291+
292+
Any new errors we add to the driver will directly extend an existing error class and no existing error will be moved to a different parent class outside of a major release.
293+
This means `BSONError.isBSONError()` will always be able to accurately capture the errors that our BSON library throws.
294+
295+
Hypothetical example: A collection in our Db has an issue with UTF-8 data:
296+
297+
```ts
298+
let documentCount = 0;
299+
const cursor = collection.find({}, { utf8Validation: true });
300+
try {
301+
for await (const doc of cursor) documentCount += 1;
302+
} catch (error) {
303+
if (BSONError.isBSONError(error)) {
304+
console.log(`Found the troublemaker UTF-8!: ${documentCount} ${error.message}`);
305+
return documentCount;
306+
}
307+
throw error;
308+
}
309+
```
310+
288311
## FAQ
289312

290313
#### Why does `undefined` get converted to `null`?

docs/upgrade-to-v5.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,27 @@ You can now find compiled bundles of the BSON library in 3 common formats in the
264264
- ES Module - `lib/bson.mjs`
265265
- Immediate Invoked Function Expression (IIFE) - `lib/bson.bundle.js`
266266
- Typically used when trying to import JS on the web CDN style, but the ES Module (`.mjs`) bundle is fully browser compatible and should be preferred if it works in your use case.
267+
268+
### `BSONTypeError` removed and `BSONError` offers filtering functionality with `static isBSONError()`
269+
270+
`BSONTypeError` has been removed because it was not a subclass of BSONError so would not return true for an `instanceof` check against `BSONError`. To learn more about our expectations of error handling see [this section of the mongodb driver's readme](https://github.com/mongodb/node-mongodb-native/tree/main#error-handling).
271+
272+
273+
A `BSONError` can be thrown from deep within a library that relies on BSON, having one error super class for the library helps with programmatic filtering of an error's origin.
274+
Since BSON can be used in environments where instances may originate from across realms, `BSONError` has a static `isBSONError()` method that helps with determining if an object is a `BSONError` instance (much like [Array.isArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)).
275+
It is our recommendation to use `isBSONError()` checks on errors and to avoid relying on parsing `error.message` and `error.name` strings in your code. We guarantee `isBSONError()` checks will pass according to semver guidelines, but errors may be sub-classed or their messages may change at any time, even patch releases, as we see fit to increase the helpfulness of the errors.
276+
277+
Hypothetical example: A collection in our Db has an issue with UTF-8 data:
278+
```ts
279+
let documentCount = 0;
280+
const cursor = collection.find({}, { utf8Validation: true });
281+
try {
282+
for await (const doc of cursor) documentCount += 1;
283+
} catch (error) {
284+
if (BSONError.isBSONError(error)) {
285+
console.log(`Found the troublemaker UTF-8!: ${documentCount} ${error.message}`);
286+
return documentCount;
287+
}
288+
throw error;
289+
}
290+
```

src/binary.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils';
22
import { isUint8Array } from './parser/utils';
33
import type { EJSONOptions } from './extended_json';
4-
import { BSONError, BSONTypeError } from './error';
4+
import { BSONError } from './error';
55
import { BSON_BINARY_SUBTYPE_UUID_NEW, BSON_MAJOR_VERSION } from './constants';
66
import { ByteUtils } from './utils/byte_utils';
77

@@ -86,7 +86,7 @@ export class Binary {
8686
!(buffer instanceof ArrayBuffer) &&
8787
!Array.isArray(buffer)
8888
) {
89-
throw new BSONTypeError(
89+
throw new BSONError(
9090
'Binary can only be constructed from string, Buffer, TypedArray, or Array<number>'
9191
);
9292
}
@@ -121,9 +121,9 @@ export class Binary {
121121
put(byteValue: string | number | Uint8Array | number[]): void {
122122
// If it's a string and a has more than one character throw an error
123123
if (typeof byteValue === 'string' && byteValue.length !== 1) {
124-
throw new BSONTypeError('only accepts single character String');
124+
throw new BSONError('only accepts single character String');
125125
} else if (typeof byteValue !== 'number' && byteValue.length !== 1)
126-
throw new BSONTypeError('only accepts single character Uint8Array or Array');
126+
throw new BSONError('only accepts single character Uint8Array or Array');
127127

128128
// Decode the byte value once
129129
let decodedByte: number;
@@ -136,7 +136,7 @@ export class Binary {
136136
}
137137

138138
if (decodedByte < 0 || decodedByte > 255) {
139-
throw new BSONTypeError('only accepts number in a valid unsigned byte range 0-255');
139+
throw new BSONError('only accepts number in a valid unsigned byte range 0-255');
140140
}
141141

142142
if (this.buffer.byteLength > this.position) {
@@ -283,7 +283,7 @@ export class Binary {
283283
data = uuidHexStringToBuffer(doc.$uuid);
284284
}
285285
if (!data) {
286-
throw new BSONTypeError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`);
286+
throw new BSONError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`);
287287
}
288288
return type === BSON_BINARY_SUBTYPE_UUID_NEW ? new UUID(data) : new Binary(data, type);
289289
}
@@ -337,7 +337,7 @@ export class UUID extends Binary {
337337
} else if (typeof input === 'string') {
338338
bytes = uuidHexStringToBuffer(input);
339339
} else {
340-
throw new BSONTypeError(
340+
throw new BSONError(
341341
'Argument passed in UUID constructor must be a UUID, a 16 byte Buffer or a 32/36 character hex string (dashes excluded/included, format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).'
342342
);
343343
}

src/bson.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export {
4949
BSONRegExp,
5050
Decimal128
5151
};
52-
export { BSONError, BSONTypeError } from './error';
52+
export { BSONError } from './error';
5353
export { BSONType } from './constants';
5454
export { EJSON } from './extended_json';
5555

src/decimal128.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BSON_MAJOR_VERSION } from './constants';
2-
import { BSONTypeError } from './error';
2+
import { BSONError } from './error';
33
import { Long } from './long';
44
import { isUint8Array } from './parser/utils';
55
import { ByteUtils } from './utils/byte_utils';
@@ -114,7 +114,7 @@ function lessThan(left: Long, right: Long): boolean {
114114
}
115115

116116
function invalidErr(string: string, message: string) {
117-
throw new BSONTypeError(`"${string}" is not a valid Decimal128 string - ${message}`);
117+
throw new BSONError(`"${string}" is not a valid Decimal128 string - ${message}`);
118118
}
119119

120120
/** @public */
@@ -147,11 +147,11 @@ export class Decimal128 {
147147
this.bytes = Decimal128.fromString(bytes).bytes;
148148
} else if (isUint8Array(bytes)) {
149149
if (bytes.byteLength !== 16) {
150-
throw new BSONTypeError('Decimal128 must take a Buffer of 16 bytes');
150+
throw new BSONError('Decimal128 must take a Buffer of 16 bytes');
151151
}
152152
this.bytes = bytes;
153153
} else {
154-
throw new BSONTypeError('Decimal128 must take a Buffer or string');
154+
throw new BSONError('Decimal128 must take a Buffer or string');
155155
}
156156
}
157157

@@ -206,7 +206,7 @@ export class Decimal128 {
206206
// TODO: implementing a custom parsing for this, or refactoring the regex would yield
207207
// further gains.
208208
if (representation.length >= 7000) {
209-
throw new BSONTypeError('' + representation + ' not a valid Decimal128 string');
209+
throw new BSONError('' + representation + ' not a valid Decimal128 string');
210210
}
211211

212212
// Results
@@ -216,7 +216,7 @@ export class Decimal128 {
216216

217217
// Validate the string
218218
if ((!stringMatch && !infMatch && !nanMatch) || representation.length === 0) {
219-
throw new BSONTypeError('' + representation + ' not a valid Decimal128 string');
219+
throw new BSONError('' + representation + ' not a valid Decimal128 string');
220220
}
221221

222222
if (stringMatch) {
@@ -288,7 +288,7 @@ export class Decimal128 {
288288
}
289289

290290
if (sawRadix && !nDigitsRead)
291-
throw new BSONTypeError('' + representation + ' not a valid Decimal128 string');
291+
throw new BSONError('' + representation + ' not a valid Decimal128 string');
292292

293293
// Read exponent if exists
294294
if (representation[index] === 'e' || representation[index] === 'E') {

src/error.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,45 @@
1-
/** @public */
1+
/**
2+
* @public
3+
* `BSONError` objects are thrown when runtime errors occur.
4+
*/
25
export class BSONError extends Error {
3-
constructor(message: string) {
4-
super(message);
6+
/**
7+
* @internal
8+
* The underlying algorithm for isBSONError may change to improve how strict it is
9+
* about determining if an input is a BSONError. But it must remain backwards compatible
10+
* with previous minors & patches of the current major version.
11+
*/
12+
protected get bsonError(): true {
13+
return true;
514
}
615

7-
get name(): string {
16+
override get name(): string {
817
return 'BSONError';
918
}
10-
}
1119

12-
/** @public */
13-
export class BSONTypeError extends TypeError {
1420
constructor(message: string) {
1521
super(message);
1622
}
1723

18-
get name(): string {
19-
return 'BSONTypeError';
24+
/**
25+
* @public
26+
*
27+
* All errors thrown from the BSON library inherit from `BSONError`.
28+
* This method can assist with determining if an error originates from the BSON library
29+
* even if it does not pass an `instanceof` check against this class' constructor.
30+
*
31+
* @param value - any javascript value that needs type checking
32+
*/
33+
public static isBSONError(value: unknown): value is BSONError {
34+
return (
35+
value != null &&
36+
typeof value === 'object' &&
37+
'bsonError' in value &&
38+
value.bsonError === true &&
39+
// Do not access the following properties, just check existence
40+
'name' in value &&
41+
'message' in value &&
42+
'stack' in value
43+
);
2044
}
2145
}

src/extended_json.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BSON_INT32_MAX, BSON_INT32_MIN, BSON_INT64_MAX, BSON_INT64_MIN } from '
55
import { DBRef, isDBRefLike } from './db_ref';
66
import { Decimal128 } from './decimal128';
77
import { Double } from './double';
8-
import { BSONError, BSONTypeError } from './error';
8+
import { BSONError } from './error';
99
import { Int32 } from './int_32';
1010
import { Long } from './long';
1111
import { MaxKey } from './max_key';
@@ -192,7 +192,7 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any {
192192
circularPart.length + (alreadySeen.length + current.length) / 2 - 1
193193
);
194194

195-
throw new BSONTypeError(
195+
throw new BSONError(
196196
'Converting circular structure to EJSON:\n' +
197197
` ${leadingPart}${alreadySeen}${circularPart}${current}\n` +
198198
` ${leadingSpace}\\${dashes}/`
@@ -324,7 +324,7 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) {
324324
// Copy the object into this library's version of that type.
325325
const mapper = BSON_TYPE_MAPPINGS[doc._bsontype];
326326
if (!mapper) {
327-
throw new BSONTypeError('Unrecognized or invalid _bsontype: ' + doc._bsontype);
327+
throw new BSONError('Unrecognized or invalid _bsontype: ' + doc._bsontype);
328328
}
329329
outDoc = mapper(outDoc);
330330
}

src/long.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BSON_MAJOR_VERSION } from './constants';
2+
import { BSONError } from './error';
23
import type { EJSONOptions } from './extended_json';
34
import type { Timestamp } from './timestamp';
45

@@ -250,7 +251,7 @@ export class Long {
250251
* @returns The corresponding Long value
251252
*/
252253
static fromString(str: string, unsigned?: boolean, radix?: number): Long {
253-
if (str.length === 0) throw Error('empty string');
254+
if (str.length === 0) throw new BSONError('empty string');
254255
if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity')
255256
return Long.ZERO;
256257
if (typeof unsigned === 'number') {
@@ -260,10 +261,10 @@ export class Long {
260261
unsigned = !!unsigned;
261262
}
262263
radix = radix || 10;
263-
if (radix < 2 || 36 < radix) throw RangeError('radix');
264+
if (radix < 2 || 36 < radix) throw new BSONError('radix');
264265

265266
let p;
266-
if ((p = str.indexOf('-')) > 0) throw Error('interior hyphen');
267+
if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen');
267268
else if (p === 0) {
268269
return Long.fromString(str.substring(1), unsigned, radix).neg();
269270
}
@@ -431,7 +432,7 @@ export class Long {
431432
*/
432433
divide(divisor: string | number | Long | Timestamp): Long {
433434
if (!Long.isLong(divisor)) divisor = Long.fromValue(divisor);
434-
if (divisor.isZero()) throw Error('division by zero');
435+
if (divisor.isZero()) throw new BSONError('division by zero');
435436

436437
// use wasm support if present
437438
if (wasm) {
@@ -959,7 +960,7 @@ export class Long {
959960
*/
960961
toString(radix?: number): string {
961962
radix = radix || 10;
962-
if (radix < 2 || 36 < radix) throw RangeError('radix');
963+
if (radix < 2 || 36 < radix) throw new BSONError('radix');
963964
if (this.isZero()) return '0';
964965
if (this.isNegative()) {
965966
// Unsigned Longs are never negative

src/objectid.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BSON_MAJOR_VERSION } from './constants';
2-
import { BSONTypeError } from './error';
2+
import { BSONError } from './error';
33
import { isUint8Array } from './parser/utils';
44
import { BSONDataView, ByteUtils } from './utils/byte_utils';
55

@@ -57,9 +57,7 @@ export class ObjectId {
5757
let workingId;
5858
if (typeof inputId === 'object' && inputId && 'id' in inputId) {
5959
if (typeof inputId.id !== 'string' && !ArrayBuffer.isView(inputId.id)) {
60-
throw new BSONTypeError(
61-
'Argument passed in must have an id that is of type string or Buffer'
62-
);
60+
throw new BSONError('Argument passed in must have an id that is of type string or Buffer');
6361
}
6462
if ('toHexString' in inputId && typeof inputId.toHexString === 'function') {
6563
workingId = ByteUtils.fromHex(inputId.toHexString());
@@ -85,17 +83,17 @@ export class ObjectId {
8583
if (bytes.byteLength === 12) {
8684
this[kId] = bytes;
8785
} else {
88-
throw new BSONTypeError('Argument passed in must be a string of 12 bytes');
86+
throw new BSONError('Argument passed in must be a string of 12 bytes');
8987
}
9088
} else if (workingId.length === 24 && checkForHexRegExp.test(workingId)) {
9189
this[kId] = ByteUtils.fromHex(workingId);
9290
} else {
93-
throw new BSONTypeError(
91+
throw new BSONError(
9492
'Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer'
9593
);
9694
}
9795
} else {
98-
throw new BSONTypeError('Argument passed in does not match the accepted types');
96+
throw new BSONError('Argument passed in does not match the accepted types');
9997
}
10098
// If we are caching the hex string
10199
if (ObjectId.cacheHexString) {
@@ -271,7 +269,7 @@ export class ObjectId {
271269
static createFromHexString(hexString: string): ObjectId {
272270
// Throw an error if it's not a valid setup
273271
if (typeof hexString === 'undefined' || (hexString != null && hexString.length !== 24)) {
274-
throw new BSONTypeError(
272+
throw new BSONError(
275273
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
276274
);
277275
}

0 commit comments

Comments
 (0)