diff --git a/packages/core/errors/exception-handler.ts b/packages/core/errors/exception-handler.ts index 3b1c47c003f..1ed86414e90 100644 --- a/packages/core/errors/exception-handler.ts +++ b/packages/core/errors/exception-handler.ts @@ -1,14 +1,21 @@ -import { RuntimeException } from './exceptions/runtime.exception'; import { Logger } from '@nestjs/common/services/logger.service'; +import { combineStackTrace } from '../helpers/combine-stack-trace'; +import { RuntimeException } from './exceptions/runtime.exception'; export class ExceptionHandler { private static readonly logger = new Logger(ExceptionHandler.name); public handle(exception: RuntimeException | Error) { if (!(exception instanceof RuntimeException)) { - ExceptionHandler.logger.error(exception.message, exception.stack); + ExceptionHandler.logger.error( + exception.message, + combineStackTrace(exception), + ); return; } - ExceptionHandler.logger.error(exception.what(), exception.stack); + ExceptionHandler.logger.error( + exception.what(), + combineStackTrace(exception), + ); } } diff --git a/packages/core/exceptions/base-exception-filter.ts b/packages/core/exceptions/base-exception-filter.ts index bc378450120..6d4d8d54a64 100644 --- a/packages/core/exceptions/base-exception-filter.ts +++ b/packages/core/exceptions/base-exception-filter.ts @@ -11,6 +11,7 @@ import { import { isObject } from '@nestjs/common/utils/shared.utils'; import { AbstractHttpAdapter } from '../adapters'; import { MESSAGES } from '../constants'; +import { combineStackTrace } from '../helpers/combine-stack-trace'; import { HttpAdapterHost } from '../helpers/http-adapter-host'; export class BaseExceptionFilter implements ExceptionFilter { @@ -71,7 +72,7 @@ export class BaseExceptionFilter implements ExceptionFilter { if (this.isExceptionObject(exception)) { return BaseExceptionFilter.logger.error( exception.message, - exception.stack, + combineStackTrace(exception), ); } return BaseExceptionFilter.logger.error(exception); diff --git a/packages/core/exceptions/external-exception-filter.ts b/packages/core/exceptions/external-exception-filter.ts index 9c6b2563b9b..b88175c668b 100644 --- a/packages/core/exceptions/external-exception-filter.ts +++ b/packages/core/exceptions/external-exception-filter.ts @@ -1,11 +1,15 @@ import { ArgumentsHost, HttpException, Logger } from '@nestjs/common'; +import { combineStackTrace } from '../helpers/combine-stack-trace'; export class ExternalExceptionFilter { private static readonly logger = new Logger('ExceptionsHandler'); catch(exception: T, host: ArgumentsHost): R | Promise { if (exception instanceof Error && !(exception instanceof HttpException)) { - ExternalExceptionFilter.logger.error(exception.message, exception.stack); + ExternalExceptionFilter.logger.error( + exception.message, + combineStackTrace(exception), + ); } throw exception; } diff --git a/packages/core/helpers/combine-stack-trace.ts b/packages/core/helpers/combine-stack-trace.ts new file mode 100644 index 00000000000..369d8828a25 --- /dev/null +++ b/packages/core/helpers/combine-stack-trace.ts @@ -0,0 +1,22 @@ +/** + * Generates the full stack trace of an error, recursively including the stack + * traces of its causes. An error may specify a cause by passing an object with + * a `cause` property as the second argument to the `Error` constructor. + * + * @param error Error whose stack trace should be generated. + * @returns A string representation of the error's stack trace. + */ +export function combineStackTrace(error: Error): string { + let result = error.stack || ''; + let errorCause = getErrorCause(error); + while (errorCause instanceof Error) { + result += '\nCaused by ' + errorCause.stack; + errorCause = getErrorCause(errorCause); + } + return result; +} + +function getErrorCause(error: Error): unknown { + // @ts-expect-error - Error.cause has been introduced in ES2022. + return error.cause; +} diff --git a/packages/core/test/errors/test/exception-handler.spec.ts b/packages/core/test/errors/test/exception-handler.spec.ts index ca3703a139e..4b3480f75f0 100644 --- a/packages/core/test/errors/test/exception-handler.spec.ts +++ b/packages/core/test/errors/test/exception-handler.spec.ts @@ -2,7 +2,7 @@ import * as sinon from 'sinon'; import { expect } from 'chai'; import { ExceptionHandler } from '../../../errors/exception-handler'; import { RuntimeException } from '../../../errors/exceptions/runtime.exception'; -import { InvalidMiddlewareException } from '../../../errors/exceptions/invalid-middleware.exception'; +import { combineStackTrace } from '../../../helpers/combine-stack-trace'; describe('ExceptionHandler', () => { let instance: ExceptionHandler; @@ -22,13 +22,16 @@ describe('ExceptionHandler', () => { it('when exception is instanceof RuntimeException', () => { const exception = new RuntimeException('msg'); instance.handle(exception); - expect(errorSpy.calledWith(exception.message, exception.stack)).to.be - .true; + expect( + errorSpy.calledWith(exception.what(), combineStackTrace(exception)), + ).to.be.true; }); it('when exception is not instanceof RuntimeException', () => { - const exception = new InvalidMiddlewareException('msg'); + const exception = new Error('msg'); instance.handle(exception); - expect(errorSpy.calledWith(exception.what(), exception.stack)).to.be.true; + expect( + errorSpy.calledWith(exception.message, combineStackTrace(exception)), + ).to.be.true; }); }); }); diff --git a/packages/core/test/helpers/combine-stack-trace.spec.ts b/packages/core/test/helpers/combine-stack-trace.spec.ts new file mode 100644 index 00000000000..dc803b81b5c --- /dev/null +++ b/packages/core/test/helpers/combine-stack-trace.spec.ts @@ -0,0 +1,42 @@ +import { combineStackTrace } from '@nestjs/core/helpers/combine-stack-trace'; +import { expect } from 'chai'; + +describe(combineStackTrace.name, () => { + it('returns error stack trace as-is when error has no cause', () => { + const error = new Error('Something went wrong'); + + const stack = combineStackTrace(error); + + expect(stack).to.equal(error.stack); + }); + + it('appends error stack trace with that of its cause', () => { + const cause = new Error('Request failed with HTTP 400'); + const error = errorWithCause('Something went wrong', cause); + + const stack = combineStackTrace(error); + + expect(stack.startsWith(error.stack)).to.be.true; + expect(stack.endsWith(cause.stack)).to.be.true; + expect(stack.includes('Caused by Error: Request failed with HTTP 400')).to + .be.true; + }); + + it('recursively appends stack traces', () => { + const cause = new Error('Request failed with HTTP 400'); + const error = errorWithCause('Unable to retrieve data', cause); + const caught = errorWithCause('Something went wrong', error); + + const stack = combineStackTrace(caught); + + expect(stack.includes('Caused by Error: Unable to retrieve data')).to.be + .true; + expect(stack.includes('Caused by Error: Request failed with HTTP 400')).to + .be.true; + }); +}); + +function errorWithCause(message: string, cause: unknown): Error { + // @ts-expect-error - Error options have been introduced in ES2022. + return new Error(message, { cause }); +} diff --git a/packages/microservices/exceptions/base-rpc-exception-filter.ts b/packages/microservices/exceptions/base-rpc-exception-filter.ts index 837330fac9c..f1a0fb2ae53 100644 --- a/packages/microservices/exceptions/base-rpc-exception-filter.ts +++ b/packages/microservices/exceptions/base-rpc-exception-filter.ts @@ -2,6 +2,7 @@ import { ArgumentsHost, Logger, RpcExceptionFilter } from '@nestjs/common'; import { isObject } from '@nestjs/common/utils/shared.utils'; import { MESSAGES } from '@nestjs/core/constants'; +import { combineStackTrace } from '@nestjs/core/helpers/combine-stack-trace'; import { Observable, throwError as _throw } from 'rxjs'; import { RpcException } from './rpc-exception'; @@ -27,7 +28,7 @@ export class BaseRpcExceptionFilter const errorMessage = MESSAGES.UNKNOWN_EXCEPTION_MESSAGE; const loggerArgs = this.isError(exception) - ? [exception.message, exception.stack] + ? [exception.message, combineStackTrace(exception)] : [exception]; const logger = BaseRpcExceptionFilter.logger; logger.error.apply(logger, loggerArgs as any); diff --git a/packages/websockets/exceptions/base-ws-exception-filter.ts b/packages/websockets/exceptions/base-ws-exception-filter.ts index bcc48b0a68b..c658aac95b0 100644 --- a/packages/websockets/exceptions/base-ws-exception-filter.ts +++ b/packages/websockets/exceptions/base-ws-exception-filter.ts @@ -1,6 +1,7 @@ import { ArgumentsHost, Logger, WsExceptionFilter } from '@nestjs/common'; import { isObject } from '@nestjs/common/utils/shared.utils'; import { MESSAGES } from '@nestjs/core/constants'; +import { combineStackTrace } from '@nestjs/core/helpers/combine-stack-trace'; import { WsException } from '../errors/ws-exception'; /** @@ -49,7 +50,7 @@ export class BaseWsExceptionFilter if (this.isExceptionObject(exception)) { return BaseWsExceptionFilter.logger.error( exception.message, - exception.stack, + combineStackTrace(exception), ); } return BaseWsExceptionFilter.logger.error(exception);