diff --git a/dev/src/bulk-writer.ts b/dev/src/bulk-writer.ts index 7ef437620..02bd6619f 100644 --- a/dev/src/bulk-writer.ts +++ b/dev/src/bulk-writer.ts @@ -21,7 +21,7 @@ import {RateLimiter} from './rate-limiter'; import {DocumentReference} from './reference'; import {Timestamp} from './timestamp'; import {Precondition, SetOptions, UpdateData} from './types'; -import {Deferred} from './util'; +import {Deferred, wrapError} from './util'; import {BatchWriteResult, WriteBatch, WriteResult} from './write-batch'; /*! @@ -188,7 +188,13 @@ class BulkCommitBatch { 'The batch should be marked as READY_TO_SEND before committing' ); this.state = BatchState.SENT; - return this.writeBatch.bulkCommit(); + + // Capture the error stack to preserve stack tracing across async calls. + const stack = Error().stack!; + + return this.writeBatch.bulkCommit().catch(err => { + throw wrapError(err, stack); + }); } /** diff --git a/dev/src/index.ts b/dev/src/index.ts index 9fdefe2fb..79b93b88d 100644 --- a/dev/src/index.ts +++ b/dev/src/index.ts @@ -53,7 +53,7 @@ import { Settings, UnaryMethod, } from './types'; -import {Deferred, isPermanentRpcError, requestTag} from './util'; +import {Deferred, isPermanentRpcError, requestTag, wrapError} from './util'; import { validateBoolean, validateFunction, @@ -908,9 +908,15 @@ export class Firestore { documentRefsOrReadOptions ); const tag = requestTag(); - return this.initializeIfNeeded(tag).then(() => - this.getAll_(documents, fieldMask, tag) - ); + + // Capture the error stack to preserve stack tracing across async calls. + const stack = Error().stack!; + + return this.initializeIfNeeded(tag) + .then(() => this.getAll_(documents, fieldMask, tag)) + .catch(err => { + throw wrapError(err, stack); + }); } /** diff --git a/dev/src/reference.ts b/dev/src/reference.ts index f05a083be..690e1f794 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -48,7 +48,7 @@ import { UpdateData, WhereFilterOp, } from './types'; -import {autoId, requestTag} from './util'; +import {autoId, requestTag, wrapError} from './util'; import { invalidArgumentMessage, validateEnumValue, @@ -1808,12 +1808,15 @@ export class Query { _get(transactionId?: Uint8Array): Promise> { const docs: Array> = []; + // Capture the error stack to preserve stack tracing across async calls. + const stack = Error().stack!; + return new Promise((resolve, reject) => { let readTime: Timestamp; this._stream(transactionId) .on('error', err => { - reject(err); + reject(wrapError(err, stack)); }) .on('data', result => { readTime = result.readTime; diff --git a/dev/src/util.ts b/dev/src/util.ts index d04253699..15d5f8dd9 100644 --- a/dev/src/util.ts +++ b/dev/src/util.ts @@ -146,3 +146,19 @@ export function isPermanentRpcError( return false; } } + +/** + * Wraps the provided error in a new error that includes the provided stack. + * + * Used to preserve stack traces across async calls. + * @private + */ +export function wrapError(err: Error | string, stack: string): Error { + // TODO(b/157506412): Remove `string` type and clean up any string errors + // that we are throwing. + if (typeof err === 'string') { + throw err; + } + err.stack += '\nCaused by: ' + stack; + return err; +} diff --git a/dev/src/write-batch.ts b/dev/src/write-batch.ts index 8c01eae1d..408845189 100644 --- a/dev/src/write-batch.ts +++ b/dev/src/write-batch.ts @@ -37,7 +37,7 @@ import { UpdateMap, } from './types'; import {DocumentData} from './types'; -import {isObject, isPlainObject, requestTag} from './util'; +import {isObject, isPlainObject, requestTag, wrapError} from './util'; import { customObjectMessage, invalidArgumentMessage, @@ -532,7 +532,12 @@ export class WriteBatch { * }); */ commit(): Promise { - return this.commit_(); + // Capture the error stack to preserve stack tracing across async calls. + const stack = Error().stack!; + + return this.commit_().catch(err => { + throw wrapError(err, stack); + }); } /** diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 37b2b6b9d..5b5822eb7 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2148,6 +2148,19 @@ describe('WriteBatch class', () => { }); }); + it('has a full stack trace if set() errors', () => { + // Use an invalid document name that the backend will reject. + const ref = randomCol.doc('__doc__'); + const batch = firestore.batch(); + batch.set(ref, {foo: 'a'}); + return batch + .commit() + .then(() => Promise.reject('commit() should have failed')) + .catch((err: Error) => { + expect(err.stack).to.contain('WriteBatch.commit'); + }); + }); + it('has update() method', () => { const ref = randomCol.doc('doc'); const batch = firestore.batch();