sentry-javascript icon indicating copy to clipboard operation
sentry-javascript copied to clipboard

NestJS errors throw an internal sentry error

Open Asynchronite opened this issue 1 month ago • 12 comments

Is there an existing issue for this?

  • [x] I have checked for existing issues https://github.com/getsentry/sentry-javascript/issues
  • [x] I have reviewed the documentation https://docs.sentry.io/
  • [x] I am using the latest SDK release https://github.com/getsentry/sentry-javascript/releases

How do you use Sentry?

Self-hosted/on-premise

Which SDK are you using?

@sentry/nestjs

SDK Version

10.29.0

Framework Version

@nestjs/core 11.1.9

Link to Sentry event

No response

Reproduction Example/SDK Setup

import 'dotenv/config';
import * as Sentry from '@sentry/nestjs';

const environment = process.env;
console.log('Instrumenting Sentry...');

Sentry.init({
	// ...
	enabled: environment.API_STAGE != 'dev',
	spotlight: environment.SENTRY_SPOTLIGHT,
	
	// ...
	
	enableLogs: true,
});

// Then elsewhere, when the nest app (Which in this case is a Discord bot) starts up, and an error gets thrown, attached below.

Steps to Reproduce

  1. Init Sentry
  2. Wait for Sentry to start up
  3. In one of the files that has code for the Discord slash command, I run throw new Error("Hello chat!!!"); to test this feature and I get that error.

Expected Result

Expected result is the error gets logged in the console and it gets sent to Sentry and Spotlight.

Actual Result

Instead of that happening, the following thing gets logged, and nothing gets sent to Sentry nor Spotlight:

Image

Additional Context

Some errors still get sent into Sentry, but this sometimes also happens..

Priority

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it.

Asynchronite avatar Dec 05 '25 08:12 Asynchronite

JS-1257

linear[bot] avatar Dec 05 '25 08:12 linear[bot]

@BYK Since you wanted to be pinged for this.

Asynchronite avatar Dec 05 '25 08:12 Asynchronite

Hi, are you using microservices and did you set up an exception filter? https://docs.sentry.io/platforms/javascript/guides/nestjs/#using-error-filters-for-specific-exception-types

s1gr1d avatar Dec 05 '25 09:12 s1gr1d

Hi, are you using microservices and did you set up an exception filter? https://docs.sentry.io/platforms/javascript/guides/nestjs/#using-error-filters-for-specific-exception-types

Hey there! Thanks for the swift reply. No, I am not using microservices, and no I did not set up an exception filter. Is this required?

Asynchronite avatar Dec 05 '25 09:12 Asynchronite

Actually, scratch that last part. We are using the @SentryExceptionCaptured() decorator in the AllExceptionsFilter. Code snippet:

// Package imports...


@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
	private static readonly _logger = new Logger('ExceptionsHandler');
	@SentryExceptionCaptured()
	catch(exception: unknown, host: ArgumentsHost) {
		if (!(exception instanceof HttpException))
			return super.catch(exception, host);

		const hostType = host.getType() as any;
		if (hostType === 'necord') {
			if (!(exception instanceof IntrinsicException)) {
				AllExceptionsFilter._logger.error(exception);
			}
			return;
		}
		super.catch(exception, host);
	}

	public isHttpError(err: any): err is { statusCode: number; message: string } {
		return err?.statusCode && err?.message;
	}
}

Asynchronite avatar Dec 05 '25 10:12 Asynchronite

I am also using this in my AppModule:

// ...
	providers: [
		{
			provide: APP_FILTER,
			useClass: SentryGlobalFilter,
		},
// ...

Asynchronite avatar Dec 05 '25 10:12 Asynchronite

Hey! Using the @SentryGlobalFilter should be redundant if you have a catch-all filter (like your AllExceptionsFilter), so you can try if removing that changes anything. Your catch-all filter setup looks correct at first glance though.

Where are you throwing your exception from (e.g. controller or service and what kind of decorators you use there)?

nicohrubec avatar Dec 05 '25 10:12 nicohrubec

Hey! Using the @SentryGlobalFilter should be redundant if you have a catch-all filter (like your AllExceptionsFilter), so you can try if removing that changes anything. Your catch-all filter setup looks correct at first glance though.

Where are you throwing your exception from (e.g. controller or service and what kind of decorators you use there)?

Hey there! Yeah, I tried disaling SentryGloalFilter in my AppModule already, however this did not fix the issue. I am throwing my exception inside of another module (Not the .module.ts file itself, but files within that module) that handled Discord bot interactions. I'm throwing it inside of a file which has the code that gets executed when said command is run by a user on Discord.

Asynchronite avatar Dec 05 '25 10:12 Asynchronite

See: https://docs.nestjs.com/recipes/necord

Asynchronite avatar Dec 05 '25 10:12 Asynchronite

Thanks for providing more info, we'll have a look.

nicohrubec avatar Dec 05 '25 10:12 nicohrubec

@Asynchronite Have you registered your AllExceptionsFilter in your providers and did you confirm that it is actually being called? Generally it would be great if you could add some more logging to the exception filter and then paste the outputs here (so we can actually see what's going on in your setup, e.g. what branch is actually called, what hostType the exception has). Trying to reproduce this the error locally atm but so far wasn't able to, so more information would help.

nicohrubec avatar Dec 09 '25 13:12 nicohrubec

Hi @nicohrubec!

I'm one of the developers that work alongside @Asynchronite in this project. Just wanted to add additional context to the matter that we're using a framework called Necord which combines both the framework of NestJS with built-in support for Discord.JS.

In regards to answering the questions you have; yes, the AllExceptionsFilter does fire and attached below is the source code and how the variables were formatted/printed.

Source code with printed statements:

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
	private static readonly _logger = new Logger('ExceptionsHandler');
	@SentryExceptionCaptured()
	catch(exception: unknown, host: ArgumentsHost) {
		console.log("AllExceptionsFilter catch triggered!")
		console.log(">>> exception=")
		console.log(exception)
		console.log("-----")
		console.log(">>> host=")
		console.log(host)
		console.log("hostType=", host.getType());

		if (!(exception instanceof HttpException)) {
			console.log("this is not a http exception")
			return super.catch(exception, host);
		}

		const hostType = host.getType() as any;


		if (hostType === 'necord') {
			if (!(exception instanceof IntrinsicException)) {
				AllExceptionsFilter._logger.error(exception);
			}
			return;
		}
		super.catch(exception, host);
	}

	public isHttpError(err: any): err is { statusCode: number; message: string } {
		return err?.statusCode && err?.message;
	}
}

Error/Logs:

api-1  | AllExceptionsFilter catch triggered!
api-1  | >>> exception=
api-1  | 757 |     if (status >= 400 && status < 500) {
api-1  | 758 |       if (status === 401 && requestData.auth) {
api-1  | 759 |         manager.setToken(null);
api-1  | 760 |       }
api-1  | 761 |       const data = await parseResponse(res);
api-1  | 762 |       throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
api-1  |                   ^
api-1  | error: Unknown interaction
api-1  |  requestBody: {
api-1  |   files: undefined,
api-1  |   json: [Object ...],
api-1  | },
api-1  |    rawError: {
api-1  |   message: "Unknown interaction",
api-1  |   code: 10062,
api-1  | },
api-1  |        code: 10062,
api-1  |      status: 404,
api-1  |      method: "POST",
api-1  |         url: "https://discord.com/api/v10/interactions/xxx/.../callback?with_response=false",
api-1  | 
api-1  |       at handleErrors (/usr/src/app/node_modules/@discordjs/rest/dist/index.js:762:13)
api-1  |       at async runRequest (/usr/src/app/node_modules/@discordjs/rest/dist/index.js:866:29)
api-1  | 
api-1  | -----
api-1  | >>> host=
api-1  | ExecutionContextHost {
api-1  |   args: [
api-1  |     [
api-1  |       [Object ...]
api-1  |     ], SlashCommandDiscovery {
api-1  |       meta: [Object ...],
api-1  |       reflector: [Object ...],
api-1  |       subcommands: Map {},
api-1  |       discovery: [Object ...],
api-1  |       contextCallback: [AsyncFunction],
api-1  |       getDescription: [Function: getDescription],
api-1  |       setSubcommand: [Function: setSubcommand],
api-1  |       ensureSubcommand: [Function: ensureSubcommand],
api-1  |       getSubcommand: [Function: getSubcommand],
api-1  |       getSubcommands: [Function: getSubcommands],
api-1  |       getRawOptions: [Function: getRawOptions],
api-1  |       getOptions: [Function: getOptions],
api-1  |       execute: [Function: execute],
api-1  |       isSlashCommand: [Function: isSlashCommand],
api-1  |       toJSON: [Function: toJSON],
api-1  |       getName: [Function: getName],
api-1  |       setGuilds: [Function: setGuilds],
api-1  |       hasGuild: [Function: hasGuild],
api-1  |       isGlobal: [Function: isGlobal],
api-1  |       getGuilds: [Function: getGuilds],
api-1  |       getClass: [Function: getClass],
api-1  |       getHandler: [Function: getHandler],
api-1  |       setDiscoveryMeta: [Function: setDiscoveryMeta],
api-1  |       setContextCallback: [Function: setContextCallback],
api-1  |       isContextMenu: [Function: isContextMenu],
api-1  |       isMessageComponent: [Function: isMessageComponent],
api-1  |       isListener: [Function: isListener],
api-1  |       isTextCommand: [Function: isTextCommand],
api-1  |       isModal: [Function: isModal],
api-1  |     }
api-1  |   ],
api-1  |   constructorRef: null,
api-1  |   handler: null,
api-1  |   contextType: "necord",
api-1  |   setType: [Function: setType],
api-1  |   getType: [Function: getType],
api-1  |   getClass: [Function: getClass],
api-1  |   getHandler: [Function: getHandler],
api-1  |   getArgs: [Function: getArgs],
api-1  |   getArgByIndex: [Function: getArgByIndex],
api-1  |   switchToRpc: [Function: switchToRpc],
api-1  |   switchToHttp: [Function: switchToHttp],
api-1  |   switchToWs: [Function: switchToWs],
api-1  | }
api-1  | hostType= necord
api-1  | this is not a http exception
api-1  | [Nest] 107  - 12/09/2025, 6:28:08 PM   ERROR [BotService] TypeError: this.raw.getHeader is not a function. (In 'this.raw.getHeader(key)', 'this.raw.getHeader' is undefined)
api-1  |     at <anonymous> (/usr/src/app/node_modules/fastify/lib/reply.js:219:49)
api-1  |     at reply (/usr/src/app/node_modules/@nestjs/platform-fastify/adapters/fastify-adapter.js:247:26)
api-1  |     at handleUnknownError (/usr/src/app/node_modules/@nestjs/core/exceptions/base-exception-filter.js:46:28)
api-1  |     at catch (/usr/src/app/node_modules/@sentry/nestjs/build/cjs/decorators.js:81:30)
api-1  |     at next (/usr/src/app/node_modules/@nestjs/core/exceptions/external-exceptions-handler.js:14:29)
api-1  |     at processTicksAndRejections (native:7:39)

RediPanda avatar Dec 09 '25 18:12 RediPanda

Hey! Thanks for providing the logs, that helps a lot.

I thought about this a bit more, I think the following is happening:

  • Your host type is necord. However, since the exception you throw is not a HTTP exception you call the BaseExceptionFilter.
  • The BaseExceptionFilter expects a HTTP execution context, but since you are in a necord context this leads to this weird behavior. We had similar issue in the past, where we were trying to handle RPC exceptions in a HTTP context, which also led to issues.
  • You have specific logic to handle exceptions in the necord context, but that logic is never reached because the exception is not a HTTPException.

I am not exactly sure why the exception is not showing up in Sentry, but probably because it crashes before we get a chance to send the event off. What you probably want to do is to move your necord host type branch before the "not http exception" branch to avoid this situation. I am hoping this should work then. If it still doesn't work, you can of course always manually capture the exception with Sentry.captureException() to make it work.

I also backlogged a ticket to add support for the necord host type in our SentryGlobalFilter, which is not currently supported. However, atm we are not spending much time on developing new features for the Nest SDK, so might take a while until this is implemented.

nicohrubec avatar Dec 11 '25 12:12 nicohrubec