Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/core/errors/exception-handler.ts
Original file line number Diff line number Diff line change
@@ -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),
);
}
}
3 changes: 2 additions & 1 deletion packages/core/exceptions/base-exception-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = any> implements ExceptionFilter<T> {
Expand Down Expand Up @@ -71,7 +72,7 @@ export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {
if (this.isExceptionObject(exception)) {
return BaseExceptionFilter.logger.error(
exception.message,
exception.stack,
combineStackTrace(exception),
);
}
return BaseExceptionFilter.logger.error(exception);
Expand Down
6 changes: 5 additions & 1 deletion packages/core/exceptions/external-exception-filter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { combineStackTrace } from '../helpers/combine-stack-trace';

export class ExternalExceptionFilter<T = any, R = any> {
private static readonly logger = new Logger('ExceptionsHandler');

catch(exception: T, host: ArgumentsHost): R | Promise<R> {
if (exception instanceof Error && !(exception instanceof HttpException)) {
ExternalExceptionFilter.logger.error(exception.message, exception.stack);
ExternalExceptionFilter.logger.error(
exception.message,
combineStackTrace(exception),
);
}
throw exception;
}
Expand Down
22 changes: 22 additions & 0 deletions packages/core/helpers/combine-stack-trace.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 8 additions & 5 deletions packages/core/test/errors/test/exception-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
});
});
});
42 changes: 42 additions & 0 deletions packages/core/test/helpers/combine-stack-trace.spec.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -27,7 +28,7 @@ export class BaseRpcExceptionFilter<T = any, R = any>
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);
Expand Down
3 changes: 2 additions & 1 deletion packages/websockets/exceptions/base-ws-exception-filter.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -49,7 +50,7 @@ export class BaseWsExceptionFilter<TError = any>
if (this.isExceptionObject(exception)) {
return BaseWsExceptionFilter.logger.error(
exception.message,
exception.stack,
combineStackTrace(exception),
);
}
return BaseWsExceptionFilter.logger.error(exception);
Expand Down