From 9f97957fada263d94d68b40f2147b79c48a4d937 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 3 Aug 2025 12:15:56 +0600 Subject: [PATCH 01/33] feat: add types for exception handling in event handler --- .../src/types/appsync-graphql.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 91d81a4a1..e8ff9b18a 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -178,6 +178,40 @@ type GraphQlBatchRouteOptions< ? { aggregate?: T; throwOnError?: never } : { aggregate?: T; throwOnError?: R }); +// #endregion Router + +// #region Exception handling + +type ExceptionSyncHandlerFn = ( + error: TError +) => unknown; + +type ExceptionHandlerFn = ( + error: TError +) => Promise; + +type ExceptionHandler = + | ExceptionSyncHandlerFn + | ExceptionHandlerFn; + +/** + * Options for handling exceptions in the event handler. + * + * @template TError - The type of error that extends the base Error class + */ +type ExceptionHandlerOptions = { + /** + * The error class/constructor to handle (must be Error or a subclass) + */ + error: TError; + /** + * The handler function to be called when the error is caught + */ + handler: ExceptionHandler; +}; + +// #endregion Exception handling + export type { RouteHandlerRegistryOptions, RouteHandlerOptions, @@ -188,4 +222,6 @@ export type { BatchResolverHandler, BatchResolverHandlerFn, BatchResolverAggregateHandlerFn, + ExceptionHandler, + ExceptionHandlerOptions, }; From b5885621c6cfe3c6ae2dc7d68e87f71591891607 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 4 Aug 2025 10:19:24 +0600 Subject: [PATCH 02/33] refactor: improve exception handling types for better type safety --- .../src/types/appsync-graphql.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index e8ff9b18a..672088851 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -182,32 +182,31 @@ type GraphQlBatchRouteOptions< // #region Exception handling -type ExceptionSyncHandlerFn = ( - error: TError -) => unknown; +type ExceptionSyncHandlerFn = (error: T) => unknown; -type ExceptionHandlerFn = ( - error: TError -) => Promise; +type ExceptionHandlerFn = (error: T) => Promise; + +type ExceptionHandler = + | ExceptionSyncHandlerFn + | ExceptionHandlerFn; -type ExceptionHandler = - | ExceptionSyncHandlerFn - | ExceptionHandlerFn; +// biome-ignore lint/suspicious/noExplicitAny: this is a generic type that is intentionally open +type ErrorClass = new (...args: any[]) => T; /** * Options for handling exceptions in the event handler. * - * @template TError - The type of error that extends the base Error class + * @template T - The type of error that extends the base Error class */ -type ExceptionHandlerOptions = { +type ExceptionHandlerOptions = { /** * The error class/constructor to handle (must be Error or a subclass) */ - error: TError; + error: ErrorClass; /** * The handler function to be called when the error is caught */ - handler: ExceptionHandler; + handler: ExceptionHandler; }; // #endregion Exception handling @@ -223,5 +222,6 @@ export type { BatchResolverHandlerFn, BatchResolverAggregateHandlerFn, ExceptionHandler, + ErrorClass, ExceptionHandlerOptions, }; From c36c93c90979d4a941a285cb727f174a84ac35a9 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 4 Aug 2025 18:56:23 +0600 Subject: [PATCH 03/33] feat: implement ExceptionHandlerRegistry for managing GraphQL exception handlers --- .../ExceptionHandlerRegistry.ts | 80 +++++++++++++++++++ .../src/types/appsync-graphql.ts | 13 +++ 2 files changed, 93 insertions(+) create mode 100644 packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts new file mode 100644 index 000000000..1ff4e5030 --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -0,0 +1,80 @@ +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; +import type { + ExceptionHandler, + ExceptionHandlerOptions, + ExceptionHandlerRegistryOptions, +} from '../types/appsync-graphql.js'; + +/** + * Registry for storing exception handlers for GraphQL resolvers in AWS AppSync GraphQL API's. + */ +class ExceptionHandlerRegistry { + /** + * A map of registered exception handlers, keyed by their error class name. + */ + protected readonly handlers: Map = new Map(); + /** + * A logger instance to be used for logging debug and warning messages. + */ + readonly #logger: Pick; + + public constructor(options: ExceptionHandlerRegistryOptions) { + this.#logger = options.logger; + } + + /** + * Registers an exception handler for a specific error class. + * + * If a handler for the given error class is already registered, it will be replaced and a warning will be logged. + * + * @param options - The options containing the error class and its associated handler. + */ + public register(options: ExceptionHandlerOptions): void { + const { error, handler } = options; + const errorName = error.name; + + this.#logger.debug(`Adding exception handler for error class ${errorName}`); + + if (this.handlers.has(errorName)) { + this.#logger.warn( + `An exception handler for error class '${errorName}' is already registered. The previous handler will be replaced.` + ); + } + + this.handlers.set(errorName, { + error, + handler: handler as ExceptionHandler, + }); + } + + /** + * Resolves and returns the appropriate exception handler for a given error instance. + * + * This method attempts to find a registered exception handler based on the error's constructor name. + * If a matching handler is found, it is returned; otherwise, `undefined` is returned. + * + * @param error - The error instance for which to resolve an exception handler. + */ + public resolve(error: Error): ExceptionHandler | undefined { + const errorName = error.constructor.name; + this.#logger.debug(`Looking for exception handler for error: ${errorName}`); + + const handlerOptions = this.handlers.get(errorName); + if (handlerOptions) { + this.#logger.debug(`Found exact match for error class: ${errorName}`); + return handlerOptions.handler; + } + + this.#logger.debug(`No exception handler found for error: ${errorName}`); + return undefined; + } + + /** + * Checks if there are any registered exception handlers. + */ + public hasHandlers(): boolean { + return this.handlers.size > 0; + } +} + +export { ExceptionHandlerRegistry }; diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 672088851..dc9e48bd3 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -209,6 +209,18 @@ type ExceptionHandlerOptions = { handler: ExceptionHandler; }; +/** + * Options for the {@link ExceptionHandlerRegistry | `ExceptionHandlerRegistry`} class + */ +type ExceptionHandlerRegistryOptions = { + /** + * A logger instance to be used for logging debug, warning, and error messages. + * + * When no logger is provided, we'll only log warnings and errors using the global `console` object. + */ + logger: Pick; +}; + // #endregion Exception handling export type { @@ -224,4 +236,5 @@ export type { ExceptionHandler, ErrorClass, ExceptionHandlerOptions, + ExceptionHandlerRegistryOptions, }; From 150157eb910dcf8f885eed069c2c0ce21497edc9 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 4 Aug 2025 19:02:39 +0600 Subject: [PATCH 04/33] feat: add exception handling support in AppSyncGraphQLResolver and Router --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 22 +++- .../src/appsync-graphql/Router.ts | 104 ++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index f6a0428e6..b1ea4714a 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -197,7 +197,7 @@ class AppSyncGraphQLResolver extends Router { try { return await fn(); } catch (error) { - return this.#handleError( + return await this.#handleError( error, `An error occurred in handler ${event.info.fieldName}` ); @@ -209,16 +209,34 @@ class AppSyncGraphQLResolver extends Router { * * Logs the provided error message and error object. If the error is an instance of * `InvalidBatchResponseException` or `ResolverNotFoundException`, it is re-thrown. + * Checks for registered exception handlers and calls them if available. * Otherwise, the error is formatted into a response using `#formatErrorResponse`. * * @param error - The error object to handle. * @param errorMessage - A descriptive message to log alongside the error. * @throws InvalidBatchResponseException | ResolverNotFoundException */ - #handleError(error: unknown, errorMessage: string) { + async #handleError(error: unknown, errorMessage: string): Promise { this.logger.error(errorMessage, error); if (error instanceof InvalidBatchResponseException) throw error; if (error instanceof ResolverNotFoundException) throw error; + if (this.exceptionHandlerRegistry.hasHandlers() && error instanceof Error) { + const exceptionHandler = this.exceptionHandlerRegistry.resolve(error); + if (exceptionHandler) { + try { + this.logger.debug( + `Calling exception handler for error: ${error.constructor.name}` + ); + return await exceptionHandler(error); + } catch (handlerError) { + this.logger.error( + `Exception handler for ${error.constructor.name} threw an error`, + handlerError + ); + } + } + } + return this.#formatErrorResponse(error); } diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 57a243650..f20d39a2f 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -5,11 +5,14 @@ import { } from '@aws-lambda-powertools/commons/utils/env'; import type { BatchResolverHandler, + ErrorClass, + ExceptionHandler, GraphQlBatchRouteOptions, GraphQlRouteOptions, GraphQlRouterOptions, ResolverHandler, } from '../types/appsync-graphql.js'; +import { ExceptionHandlerRegistry } from './ExceptionHandlerRegistry.js'; import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; /** @@ -24,6 +27,10 @@ class Router { * A map of registered routes for GraphQL batch events, keyed by their fieldNames. */ protected readonly batchResolverRegistry: RouteHandlerRegistry; + /** + * A map of registered exception handlers for handling errors in GraphQL resolvers. + */ + protected readonly exceptionHandlerRegistry: ExceptionHandlerRegistry; /** * A logger instance to be used for logging debug, warning, and error messages. * @@ -51,6 +58,9 @@ class Router { this.batchResolverRegistry = new RouteHandlerRegistry({ logger: this.logger, }); + this.exceptionHandlerRegistry = new ExceptionHandlerRegistry({ + logger: this.logger, + }); this.isDev = isDevMode(); } @@ -946,6 +956,100 @@ class Router { return descriptor; }; } + + /** + * Register an exception handler for a specific error class. + * + * Registers a handler for a specific error class that can be thrown by GraphQL resolvers. + * The handler will be invoked when an error of the specified class (or its subclasses) is thrown + * from any resolver function. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * // Register an exception handler for AssertionError + * app.exceptionHandler(AssertionError, async (error) => { + * return { + * message: 'Assertion failed', + * details: error.message + * }; + * }); + * + * // Register a resolver that might throw an AssertionError + * app.onQuery('getTodo', async () => { + * throw new AssertionError('Something went wrong'); + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * As a decorator: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * ⁣@app.exceptionHandler(AssertionError) + * async handleAssertionError(error) { + * return { + * message: 'Assertion failed', + * details: error.message + * }; + * } + * + * ⁣@app.onQuery('getUser') + * async getUser() { + * throw new AssertionError('Something went wrong'); + * } + * + * async handler(event, context) { + * return app.resolve(event, context, { + * scope: this, // bind decorated methods to the class instance + * }); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param errorClass - The error class to handle. + * @param handler - The handler function to be called when the error is caught. + */ + public exceptionHandler( + error: ErrorClass, + handler: ExceptionHandler + ): void; + public exceptionHandler( + error: ErrorClass + ): MethodDecorator; + public exceptionHandler( + error: ErrorClass, + handler?: ExceptionHandler + ): MethodDecorator | undefined { + if (typeof handler === 'function') { + this.exceptionHandlerRegistry.register({ + error, + handler: handler as ExceptionHandler, + }); + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + this.exceptionHandlerRegistry.register({ + error, + handler: descriptor?.value, + }); + return descriptor; + }; + } } export { Router }; From 432f5d6c4724f21f795cbd745325fe0eefd51b0a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 4 Aug 2025 19:28:56 +0600 Subject: [PATCH 05/33] test: add unit tests for ExceptionHandlerRegistry functionality --- .../ExceptionHandlerRegistry.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts diff --git a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts new file mode 100644 index 000000000..f9c1258c7 --- /dev/null +++ b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts @@ -0,0 +1,78 @@ +import { ExceptionHandlerRegistry } from 'src/appsync-graphql/ExceptionHandlerRegistry.js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { ExceptionHandlerOptions } from '../../../src/types/appsync-graphql.js'; + +describe('Class: ExceptionHandlerRegistry', () => { + class MockExceptionHandlerRegistry extends ExceptionHandlerRegistry { + public declare handlers: Map; + } + class CustomError extends Error {} + + const getRegistry = () => + new MockExceptionHandlerRegistry({ logger: console }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('registers an exception handler for an error class', () => { + // Prepare + const handler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: CustomError, handler }); + + // Assess + expect(registry.handlers.size).toBe(1); + expect(registry.handlers.get('CustomError')).toBeDefined(); + }); + + it('logs a warning and replaces the previous handler if the error class is already registered', () => { + // Prepare + const originalHandler = vi.fn(); + const otherHandler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: CustomError, handler: originalHandler }); + registry.register({ error: CustomError, handler: otherHandler }); + + // Assess + expect(registry.handlers.size).toBe(1); + expect(registry.handlers.get('CustomError')).toEqual({ + error: CustomError, + handler: otherHandler, + }); + expect(console.warn).toHaveBeenCalledWith( + "An exception handler for error class 'CustomError' is already registered. The previous handler will be replaced." + ); + }); + + it('resolve returns the correct handler for a registered error instance', () => { + // Prepare + const handler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: CustomError, handler }); + const resolved = registry.resolve(new CustomError('fail')); + + // Assess + expect(resolved).toBe(handler); + }); + + it('resolve returns undefined if no handler is registered for the error', () => { + // Prepare + class OtherError extends Error {} + const handler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: CustomError, handler }); + const resolved = registry.resolve(new OtherError('fail')); + + // Assess + expect(resolved).toBeUndefined(); + }); +}); From 081e805f81d90c2fa31d5629eeabe0dd29f79a4d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 6 Aug 2025 19:29:15 +0600 Subject: [PATCH 06/33] feat: add exception handling for ValidationError, NotFoundError, and DatabaseError in AppSyncGraphQLResolver --- .../AppSyncGraphQLResolver.test.ts | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 95cbcc25f..bcb1cc440 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1,4 +1,5 @@ import context from '@aws-lambda-powertools/testing-utils/context'; +import { AssertionError } from 'assert'; import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js'; @@ -8,6 +9,27 @@ import { } from '../../../src/appsync-graphql/index.js'; import { onGraphqlEventFactory } from '../../helpers/factories.js'; +class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } +} + +class NotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'NotFoundError'; + } +} + +class DatabaseError extends Error { + constructor(message: string) { + super(message); + this.name = 'DatabaseError'; + } +} + describe('Class: AppSyncGraphQLResolver', () => { beforeEach(() => { vi.clearAllMocks(); @@ -706,4 +728,282 @@ describe('Class: AppSyncGraphQLResolver', () => { expect(resultMutation).toEqual(['scoped', 'scoped']); } ); + + // #region Exception Handling + + it('should register and use an exception handler for specific error types', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler(ValidationError, async (error) => { + return { + message: 'Validation failed', + details: error.message, + type: 'validation_error', + }; + }); + + app.onQuery<{ id: string }>('getUser', async ({ id }) => { + if (!id) { + throw new ValidationError('User ID is required'); + } + return { id, name: 'John Doe' }; + }); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + message: 'Validation failed', + details: 'User ID is required', + type: 'validation_error', + }); + }); + + it('should handle multiple different error types with specific handlers', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler(ValidationError, async (error) => { + return { + message: 'Validation failed', + details: error.message, + type: 'validation_error', + }; + }); + + app.exceptionHandler(NotFoundError, async (error) => { + return { + message: 'Resource not found', + details: error.message, + type: 'not_found_error', + }; + }); + + app.onQuery<{ id: string }>('getUser', async ({ id }) => { + if (!id) { + throw new ValidationError('User ID is required'); + } + if (id === 'not-found') { + throw new NotFoundError(`User with ID ${id} not found`); + } + return { id, name: 'John Doe' }; + }); + + // Act + const validationResult = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + const notFoundResult = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: 'not-found' }), + context + ); + + // Asses + expect(validationResult).toEqual({ + message: 'Validation failed', + details: 'User ID is required', + type: 'validation_error', + }); + expect(notFoundResult).toEqual({ + message: 'Resource not found', + details: 'User with ID not-found not found', + type: 'not_found_error', + }); + }); + + it('should prefer exact error class match over inheritance match', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler(Error, async (error) => { + return { + message: 'Generic error occurred', + details: error.message, + type: 'generic_error', + }; + }); + + app.exceptionHandler(ValidationError, async (error) => { + return { + message: 'Validation failed', + details: error.message, + type: 'validation_error', + }; + }); + + app.onQuery('getUser', async () => { + throw new ValidationError('Specific validation error'); + }); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + message: 'Validation failed', + details: 'Specific validation error', + type: 'validation_error', + }); + }); + + it('should fall back to default error formatting when no exception handler is found', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler(AssertionError, async (error) => { + return { + message: 'Validation failed', + details: error.message, + type: 'validation_error', + }; + }); + + app.onQuery('getUser', async () => { + throw new DatabaseError('Database connection failed'); + }); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + error: 'DatabaseError - Database connection failed', + }); + }); + + it('should fall back to default error formatting when exception handler throws an error', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + app.exceptionHandler(ValidationError, async () => { + throw new Error('Exception handler failed'); + }); + + app.onQuery('getUser', async () => { + throw new ValidationError('Original error'); + }); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + error: 'ValidationError - Original error', + }); + expect(console.error).toHaveBeenNthCalledWith( + 2, + 'Exception handler for ValidationError threw an error', + new Error('Exception handler failed') + ); + }); + + it('should work with async exception handlers', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler(ValidationError, async (error) => { + return { + message: 'Async validation failed', + details: error.message, + type: 'async_validation_error', + }; + }); + + app.onQuery('getUser', async () => { + throw new ValidationError('Async error test'); + }); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + message: 'Async validation failed', + details: 'Async error test', + type: 'async_validation_error', + }); + }); + + it('should not interfere with ResolverNotFoundException', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler(RangeError, async (error) => { + return { + message: 'This should not be called', + details: error.message, + type: 'should_not_happen', + }; + }); + + // Act & Assess + await expect( + app.resolve( + onGraphqlEventFactory('nonExistentResolver', 'Query'), + context + ) + ).rejects.toThrow('No resolver found for Query-nonExistentResolver'); + }); + + it('should work as a method decorator', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + class TestService { + @app.exceptionHandler(ValidationError) + async handleValidationError(error: ValidationError) { + return { + message: 'Decorator validation failed', + details: error.message, + type: 'decorator_validation_error', + }; + } + + @app.onQuery('getUser') + async getUser() { + throw new ValidationError('Decorator error test'); + } + + async handler(event: unknown, context: Context) { + return app.resolve(event, context, { + scope: this, + }); + } + } + + const service = new TestService(); + + // Act + const result = await service.handler( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + message: 'Decorator validation failed', + details: 'Decorator error test', + type: 'decorator_validation_error', + }); + }); + + // #endregion Exception handling }); From b7a28468547355ea328e79e27aaacaf138c5e0c6 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 6 Aug 2025 19:45:38 +0600 Subject: [PATCH 07/33] fix: correct import path for ExceptionHandlerRegistry in unit tests --- .../tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts index f9c1258c7..59923dfaa 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts @@ -1,5 +1,5 @@ -import { ExceptionHandlerRegistry } from 'src/appsync-graphql/ExceptionHandlerRegistry.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ExceptionHandlerRegistry } from '../../../src/appsync-graphql/ExceptionHandlerRegistry.js'; import type { ExceptionHandlerOptions } from '../../../src/types/appsync-graphql.js'; describe('Class: ExceptionHandlerRegistry', () => { From d1a499dc5fc336b1b402b5f7a03340757e83333c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 6 Aug 2025 19:58:58 +0600 Subject: [PATCH 08/33] fix: update import path for Router in unit tests --- .../event-handler/tests/unit/appsync-graphql/Router.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index 6dcd7dca2..5c5b20301 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -1,5 +1,5 @@ -import { Router } from 'src/appsync-graphql/Router.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Router } from '../../../src/appsync-graphql/Router.js'; describe('Class: Router', () => { beforeEach(() => { From 2e1b853ed34d0944060db3142708589b9f717cab Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 6 Aug 2025 20:11:31 +0600 Subject: [PATCH 09/33] fix: update exceptionHandler method signature for improved type handling --- .../event-handler/src/appsync-graphql/Router.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index f20d39a2f..482926b7f 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -1023,16 +1023,16 @@ class Router { * @param errorClass - The error class to handle. * @param handler - The handler function to be called when the error is caught. */ - public exceptionHandler( - error: ErrorClass, - handler: ExceptionHandler + public exceptionHandler( + error: ErrorClass, + handler: ExceptionHandler ): void; - public exceptionHandler( - error: ErrorClass + public exceptionHandler( + error: ErrorClass ): MethodDecorator; - public exceptionHandler( - error: ErrorClass, - handler?: ExceptionHandler + public exceptionHandler( + error: ErrorClass, + handler?: ExceptionHandler ): MethodDecorator | undefined { if (typeof handler === 'function') { this.exceptionHandlerRegistry.register({ From 3dc04b65f3e437577cdfba4be2b2aa6d4d250346 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 6 Aug 2025 22:18:03 +0600 Subject: [PATCH 10/33] fix: update AssertionError usage in examples for consistency --- packages/event-handler/src/appsync-graphql/Router.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 482926b7f..2de7a2c9f 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -967,6 +967,7 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import { AssertionError } from 'assert'; * * const app = new AppSyncGraphQLResolver(); * @@ -980,7 +981,7 @@ class Router { * * // Register a resolver that might throw an AssertionError * app.onQuery('getTodo', async () => { - * throw new AssertionError('Something went wrong'); + * throw new AssertionError(); * }); * * export const handler = async (event, context) => @@ -992,12 +993,13 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import { AssertionError } from 'assert'; * * const app = new AppSyncGraphQLResolver(); * * class Lambda { * ⁣@app.exceptionHandler(AssertionError) - * async handleAssertionError(error) { + * async handleAssertionError(error: AssertionError) { * return { * message: 'Assertion failed', * details: error.message @@ -1006,7 +1008,7 @@ class Router { * * ⁣@app.onQuery('getUser') * async getUser() { - * throw new AssertionError('Something went wrong'); + * throw new AssertionError(); * } * * async handler(event, context) { From 5df6159b9d54ba03e47923c10a2581bc466fb4f2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:01:16 +0600 Subject: [PATCH 11/33] feat: enhance exception handling by registering handlers for multiple error types in AppSyncGraphQLResolver --- .../AppSyncGraphQLResolver.test.ts | 93 +++++++++++++------ 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index bcb1cc440..557850dda 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -7,6 +7,7 @@ import { InvalidBatchResponseException, ResolverNotFoundException, } from '../../../src/appsync-graphql/index.js'; +import type { ErrorClass } from '../../../src/types/appsync-graphql.js'; import { onGraphqlEventFactory } from '../../helpers/factories.js'; class ValidationError extends Error { @@ -731,38 +732,74 @@ describe('Class: AppSyncGraphQLResolver', () => { // #region Exception Handling - it('should register and use an exception handler for specific error types', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); + it.each([ + { + errorClass: EvalError, + message: 'Evaluation failed', + }, + { + errorClass: RangeError, + message: 'Range failed', + }, + { + errorClass: ReferenceError, + message: 'Reference failed', + }, + { + errorClass: SyntaxError, + message: 'Syntax missing', + }, + { + errorClass: TypeError, + message: 'Type failed', + }, + { + errorClass: URIError, + message: 'URI failed', + }, + { + errorClass: AggregateError, + message: 'Aggregation failed', + }, + ])( + 'should register handler for %s', + async ({ + errorClass, + message, + }: { + errorClass: ErrorClass; + message: string; + }) => { + // Prepare + const app = new AppSyncGraphQLResolver(); - app.exceptionHandler(ValidationError, async (error) => { - return { - message: 'Validation failed', - details: error.message, - type: 'validation_error', - }; - }); + app.exceptionHandler(errorClass, async (err) => { + return { + message: err.message, + errorName: err.constructor.name, + }; + }); - app.onQuery<{ id: string }>('getUser', async ({ id }) => { - if (!id) { - throw new ValidationError('User ID is required'); - } - return { id, name: 'John Doe' }; - }); + app.onQuery('getUser', async () => { + if (errorClass === AggregateError) { + throw new errorClass([new Error()], message); + } + throw new errorClass(message); + }); - // Act - const result = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', {}), - context - ); + // Act + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', {}), + context + ); - // Assess - expect(result).toEqual({ - message: 'Validation failed', - details: 'User ID is required', - type: 'validation_error', - }); - }); + // Assess + expect(result).toEqual({ + message, + errorName: errorClass.name, + }); + } + ); it('should handle multiple different error types with specific handlers', async () => { // Prepare From 32919daed26e1a47e7574f5ac1d1efd789113008 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:03:37 +0600 Subject: [PATCH 12/33] fix: update error message formatting in exception handler for clarity --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 557850dda..baf8783a7 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -775,7 +775,7 @@ describe('Class: AppSyncGraphQLResolver', () => { app.exceptionHandler(errorClass, async (err) => { return { - message: err.message, + message, errorName: err.constructor.name, }; }); From d024aad9e7ade717c857b9c47741061d7b584782 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:05:10 +0600 Subject: [PATCH 13/33] fix: update NotFoundError handling to use '0' as the identifier for not found cases --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index baf8783a7..9de4412f9 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -825,7 +825,7 @@ describe('Class: AppSyncGraphQLResolver', () => { if (!id) { throw new ValidationError('User ID is required'); } - if (id === 'not-found') { + if (id === '0') { throw new NotFoundError(`User with ID ${id} not found`); } return { id, name: 'John Doe' }; @@ -837,7 +837,7 @@ describe('Class: AppSyncGraphQLResolver', () => { context ); const notFoundResult = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', { id: 'not-found' }), + onGraphqlEventFactory('getUser', 'Query', { id: '0' }), context ); @@ -849,7 +849,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); expect(notFoundResult).toEqual({ message: 'Resource not found', - details: 'User with ID not-found not found', + details: 'User with ID 0 not found', type: 'not_found_error', }); }); From 66ff42e69bd2aa65fe309d19b2d0ac3b3554ca8f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:06:35 +0600 Subject: [PATCH 14/33] fix: improve error handling in exception handler test for ValidationError --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 9de4412f9..ee480568c 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -923,9 +923,10 @@ describe('Class: AppSyncGraphQLResolver', () => { it('should fall back to default error formatting when exception handler throws an error', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); + const errorToBeThrown = new Error('Exception handler failed'); app.exceptionHandler(ValidationError, async () => { - throw new Error('Exception handler failed'); + throw errorToBeThrown; }); app.onQuery('getUser', async () => { @@ -945,7 +946,7 @@ describe('Class: AppSyncGraphQLResolver', () => { expect(console.error).toHaveBeenNthCalledWith( 2, 'Exception handler for ValidationError threw an error', - new Error('Exception handler failed') + errorToBeThrown ); }); From aac7f9fefc6a0d8f0db6fddde8aeb5cf5382c28b Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:07:39 +0600 Subject: [PATCH 15/33] fix: update exception handler test to use synchronous handling for ValidationError --- .../appsync-graphql/AppSyncGraphQLResolver.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index ee480568c..f126442a6 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -950,20 +950,20 @@ describe('Class: AppSyncGraphQLResolver', () => { ); }); - it('should work with async exception handlers', async () => { + it('should work with sync exception handlers', async () => { // Prepare const app = new AppSyncGraphQLResolver(); - app.exceptionHandler(ValidationError, async (error) => { + app.exceptionHandler(ValidationError, (error) => { return { - message: 'Async validation failed', + message: 'Sync validation failed', details: error.message, - type: 'async_validation_error', + type: 'sync_validation_error', }; }); app.onQuery('getUser', async () => { - throw new ValidationError('Async error test'); + throw new ValidationError('Sync error test'); }); // Act @@ -974,8 +974,8 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(result).toEqual({ - message: 'Async validation failed', - details: 'Async error test', + message: 'Sync validation failed', + details: 'Sync error test', type: 'async_validation_error', }); }); From ad1793e68cab18dfad018180b0c75a5911a3ab4c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:23:31 +0600 Subject: [PATCH 16/33] fix: update test descriptions to clarify exception handling behavior in AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index f126442a6..bc3b82111 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -762,7 +762,7 @@ describe('Class: AppSyncGraphQLResolver', () => { message: 'Aggregation failed', }, ])( - 'should register handler for %s', + 'should invoke exception handler for %s', async ({ errorClass, message, @@ -801,7 +801,7 @@ describe('Class: AppSyncGraphQLResolver', () => { } ); - it('should handle multiple different error types with specific handlers', async () => { + it('should handle multiple different error types with specific exception handlers', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -950,7 +950,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ); }); - it('should work with sync exception handlers', async () => { + it('should invoke sync exception handlers and return their result', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -976,11 +976,11 @@ describe('Class: AppSyncGraphQLResolver', () => { expect(result).toEqual({ message: 'Sync validation failed', details: 'Sync error test', - type: 'async_validation_error', + type: 'sync_validation_error', }); }); - it('should not interfere with ResolverNotFoundException', async () => { + it('does not catch ResolverNotFoundException with unrelated exception handlers', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1001,7 +1001,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ).rejects.toThrow('No resolver found for Query-nonExistentResolver'); }); - it('should work as a method decorator', async () => { + it('invokes exception handler when used as a method decorator', async () => { // Prepare const app = new AppSyncGraphQLResolver(); From 737248daa17d9f21e2beee49063df6d49b01e7c2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 8 Aug 2025 20:31:05 +0600 Subject: [PATCH 17/33] fix: improve test descriptions for clarity in AppSyncGraphQLResolver --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index bc3b82111..bd57a632b 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -980,7 +980,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('does not catch ResolverNotFoundException with unrelated exception handlers', async () => { + it('should not interfere with ResolverNotFoundException', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1001,7 +1001,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ).rejects.toThrow('No resolver found for Query-nonExistentResolver'); }); - it('invokes exception handler when used as a method decorator', async () => { + it('should work as a method decorator', async () => { // Prepare const app = new AppSyncGraphQLResolver(); From e93530cee9a1e360a10fd1491685519a4fbd2211 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 11:17:08 +0600 Subject: [PATCH 18/33] fix: update error formatting to use constructor name in AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- .../AppSyncGraphQLResolver.test.ts | 23 +++---------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index b1ea4714a..8677efc9f 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -398,7 +398,7 @@ class AppSyncGraphQLResolver extends Router { #formatErrorResponse(error: unknown) { if (error instanceof Error) { return { - error: `${error.name} - ${error.message}`, + error: `${error.constructor.name} - ${error.message}`, }; } return { diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index bd57a632b..ce0b60c82 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -10,26 +10,9 @@ import { import type { ErrorClass } from '../../../src/types/appsync-graphql.js'; import { onGraphqlEventFactory } from '../../helpers/factories.js'; -class ValidationError extends Error { - constructor(message: string) { - super(message); - this.name = 'ValidationError'; - } -} - -class NotFoundError extends Error { - constructor(message: string) { - super(message); - this.name = 'NotFoundError'; - } -} - -class DatabaseError extends Error { - constructor(message: string) { - super(message); - this.name = 'DatabaseError'; - } -} +class ValidationError extends Error {} +class NotFoundError extends Error {} +class DatabaseError extends Error {} describe('Class: AppSyncGraphQLResolver', () => { beforeEach(() => { From 737a76631faf8e34d68ec5a9b4f255bcba0e6168 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 11:32:50 +0600 Subject: [PATCH 19/33] fix: simplify error throwing logic in onQuery handler for better readability --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index ce0b60c82..3964bc91c 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -764,10 +764,9 @@ describe('Class: AppSyncGraphQLResolver', () => { }); app.onQuery('getUser', async () => { - if (errorClass === AggregateError) { - throw new errorClass([new Error()], message); - } - throw new errorClass(message); + throw errorClass === AggregateError + ? new errorClass([new Error()], message) + : new errorClass(message); }); // Act From b4d307c05429b19329a36b12ff740ec7d0edec24 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 11:42:40 +0600 Subject: [PATCH 20/33] fix: update error handling to use error name instead of constructor name for better clarity --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- .../ExceptionHandlerRegistry.ts | 2 +- .../AppSyncGraphQLResolver.test.ts | 21 ++++++++++++++++--- .../ExceptionHandlerRegistry.test.ts | 7 ++++++- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 8677efc9f..b1ea4714a 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -398,7 +398,7 @@ class AppSyncGraphQLResolver extends Router { #formatErrorResponse(error: unknown) { if (error instanceof Error) { return { - error: `${error.constructor.name} - ${error.message}`, + error: `${error.name} - ${error.message}`, }; } return { diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts index 1ff4e5030..4029e1be5 100644 --- a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -56,7 +56,7 @@ class ExceptionHandlerRegistry { * @param error - The error instance for which to resolve an exception handler. */ public resolve(error: Error): ExceptionHandler | undefined { - const errorName = error.constructor.name; + const errorName = error.name; this.#logger.debug(`Looking for exception handler for error: ${errorName}`); const handlerOptions = this.handlers.get(errorName); diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 3964bc91c..497186129 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -10,9 +10,24 @@ import { import type { ErrorClass } from '../../../src/types/appsync-graphql.js'; import { onGraphqlEventFactory } from '../../helpers/factories.js'; -class ValidationError extends Error {} -class NotFoundError extends Error {} -class DatabaseError extends Error {} +class ValidationError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'ValidationError'; + } +} +class NotFoundError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'NotFoundError'; + } +} +class DatabaseError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'DatabaseError'; + } +} describe('Class: AppSyncGraphQLResolver', () => { beforeEach(() => { diff --git a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts index 59923dfaa..68ea0359b 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts @@ -6,7 +6,12 @@ describe('Class: ExceptionHandlerRegistry', () => { class MockExceptionHandlerRegistry extends ExceptionHandlerRegistry { public declare handlers: Map; } - class CustomError extends Error {} + class CustomError extends Error { + constructor(message: string) { + super(message); + this.name = 'CustomError'; + } + } const getRegistry = () => new MockExceptionHandlerRegistry({ logger: console }); From 22433a6378ab10e39090c6debffebd35a809afd8 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 11:46:44 +0600 Subject: [PATCH 21/33] doc: update documentation to clarify that exception handler resolution is based on error class name --- .../src/appsync-graphql/ExceptionHandlerRegistry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts index 4029e1be5..714e37772 100644 --- a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -50,7 +50,7 @@ class ExceptionHandlerRegistry { /** * Resolves and returns the appropriate exception handler for a given error instance. * - * This method attempts to find a registered exception handler based on the error's constructor name. + * This method attempts to find a registered exception handler based on the error class name. * If a matching handler is found, it is returned; otherwise, `undefined` is returned. * * @param error - The error instance for which to resolve an exception handler. From eb049cfa192833203a7dada77ee39e5221c57366 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 11:50:22 +0600 Subject: [PATCH 22/33] doc: add missing region end comments for better code organization --- packages/event-handler/src/types/appsync-graphql.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index dc9e48bd3..32d7b83dd 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -60,6 +60,7 @@ type BatchResolverHandler< : | BatchResolverHandlerFn | BatchResolverSyncHandlerFn; +//#endregion // #region Resolver fn @@ -83,6 +84,8 @@ type ResolverHandler> = | ResolverSyncHandlerFn | ResolverHandlerFn; +//#endregion + // #region Resolver registry /** @@ -134,6 +137,8 @@ type RouteHandlerOptions< throwOnError?: R; }; +//#endregion + // #region Router /** @@ -178,7 +183,7 @@ type GraphQlBatchRouteOptions< ? { aggregate?: T; throwOnError?: never } : { aggregate?: T; throwOnError?: R }); -// #endregion Router +//#endregion // #region Exception handling @@ -221,7 +226,7 @@ type ExceptionHandlerRegistryOptions = { logger: Pick; }; -// #endregion Exception handling +//#endregion export type { RouteHandlerRegistryOptions, From 47c6c6a57365ed4581e958f7eeb939b14eb734df Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 12:00:08 +0600 Subject: [PATCH 23/33] test: add console error expectation for ValidationError in getUser handler --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 497186129..bac204863 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -940,6 +940,11 @@ describe('Class: AppSyncGraphQLResolver', () => { expect(result).toEqual({ error: 'ValidationError - Original error', }); + expect(console.error).toHaveBeenNthCalledWith( + 1, + 'An error occurred in handler getUser', + new ValidationError('Original error') + ); expect(console.error).toHaveBeenNthCalledWith( 2, 'Exception handler for ValidationError threw an error', From 3a7d8dc4a9486a0610b684a56bc38f692f39338f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 12:01:11 +0600 Subject: [PATCH 24/33] fix: update sync validation error message for clarity --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index bac204863..818294717 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -958,7 +958,7 @@ describe('Class: AppSyncGraphQLResolver', () => { app.exceptionHandler(ValidationError, (error) => { return { - message: 'Sync validation failed', + message: 'This is a sync handler', details: error.message, type: 'sync_validation_error', }; @@ -976,7 +976,7 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(result).toEqual({ - message: 'Sync validation failed', + message: 'This is a sync handler', details: 'Sync error test', type: 'sync_validation_error', }); From 69ed880170694b9885641ad64701cdc7cdb2efb9 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 12:06:05 +0600 Subject: [PATCH 25/33] feat: enhance error handling in AppSyncGraphQLResolver with NotFoundError decorator --- .../AppSyncGraphQLResolver.test.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 818294717..2a665ae7f 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1007,7 +1007,7 @@ describe('Class: AppSyncGraphQLResolver', () => { // Prepare const app = new AppSyncGraphQLResolver(); - class TestService { + class Lambda { @app.exceptionHandler(ValidationError) async handleValidationError(error: ValidationError) { return { @@ -1017,9 +1017,24 @@ describe('Class: AppSyncGraphQLResolver', () => { }; } + @app.exceptionHandler(NotFoundError) + handleNotFoundError(error: NotFoundError) { + return { + message: 'Decorator user not found', + details: error.message, + type: 'decorator_user_not_found', + }; + } + @app.onQuery('getUser') - async getUser() { - throw new ValidationError('Decorator error test'); + async getUser({ id, name }: { id: string; name: string }) { + if (!id) { + throw new ValidationError('Decorator error test'); + } + if (id === '0') { + throw new NotFoundError(`User with ID ${id} not found`); + } + return { id, name }; } async handler(event: unknown, context: Context) { @@ -1029,20 +1044,30 @@ describe('Class: AppSyncGraphQLResolver', () => { } } - const service = new TestService(); + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); // Act - const result = await service.handler( + const validationError = await handler( onGraphqlEventFactory('getUser', 'Query', {}), context ); + const notFoundError = await handler( + onGraphqlEventFactory('getUser', 'Query', { id: '0', name: 'John Doe' }), + context + ); // Assess - expect(result).toEqual({ + expect(validationError).toEqual({ message: 'Decorator validation failed', details: 'Decorator error test', type: 'decorator_validation_error', }); + expect(notFoundError).toEqual({ + message: 'Decorator user not found', + details: 'User with ID 0 not found', + type: 'decorator_user_not_found', + }); }); // #endregion Exception handling From e51c43ba43669d0ee3070c654042e93f07495df2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 10 Aug 2025 12:09:08 +0600 Subject: [PATCH 26/33] fix: correct handler variable usage in ExceptionHandlerRegistry tests --- .../unit/appsync-graphql/ExceptionHandlerRegistry.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts index 68ea0359b..2672221b2 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts @@ -56,15 +56,17 @@ describe('Class: ExceptionHandlerRegistry', () => { it('resolve returns the correct handler for a registered error instance', () => { // Prepare - const handler = vi.fn(); + const customErrorHandler = vi.fn(); + const rangeErrorHandler = vi.fn(); const registry = getRegistry(); // Act - registry.register({ error: CustomError, handler }); + registry.register({ error: CustomError, handler: customErrorHandler }); + registry.register({ error: RangeError, handler: rangeErrorHandler }); const resolved = registry.resolve(new CustomError('fail')); // Assess - expect(resolved).toBe(handler); + expect(resolved).toBe(customErrorHandler); }); it('resolve returns undefined if no handler is registered for the error', () => { From e018758e26632170dc59f0f6fd123214db128e76 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Aug 2025 20:38:26 +0600 Subject: [PATCH 27/33] doc: exception handling support in appsync-graphql --- docs/Dockerfile | 2 +- .../features/event-handler/appsync-graphql.md | 24 +++++++++++++++++++ .../appsync-graphql/exceptionHandling.ts | 22 +++++++++++++++++ .../exceptionHandlingResponse.json | 23 ++++++++++++++++++ .../exceptionHandlingResponseMapping.vtl | 5 ++++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts create mode 100644 examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponse.json create mode 100644 examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponseMapping.vtl diff --git a/docs/Dockerfile b/docs/Dockerfile index 29485aa41..984bd10f6 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -2,7 +2,7 @@ FROM squidfunk/mkdocs-material@sha256:bb7b015690d9fb5ef0dbc98ca3520f153aa43129fb96aec5ca54c9154dc3b729 # Install Node.js -RUN apk add --no-cache nodejs=22.13.1-r0 npm +RUN apk add --no-cache nodejs=22.15.1-r0 npm COPY requirements.txt /tmp/ RUN pip install --require-hashes -r /tmp/requirements.txt diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index e782593fb..394b460dd 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -148,6 +148,30 @@ You can access the original Lambda event or context for additional information. 1. The `event` parameter contains the original AppSync event and has type `AppSyncResolverEvent` from the `@types/aws-lambda`. +### Exception Handling + +You can use the **`exceptionHandler`** method with any exception. This allows you to handle a common exception outside your resolver. + +When using exception handlers, you'll also need to configure your AppSync response mapping template to properly handle the custom error responses. + +=== "Exception Handling" + + ```typescript hl_lines="11-13 16-18" + --8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts" + ``` + +=== "VTL Response Mapping Template" + + ```velocity hl_lines="1-3" + --8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponseMapping.vtl" + ``` + +=== "Exception Handling response" + + ```json hl_lines="11 20" + --8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponse.json" + ``` + ### Logging By default, the utility uses the global `console` logger and emits only warnings and errors. diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts new file mode 100644 index 000000000..959bf3a57 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts @@ -0,0 +1,22 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { AssertionError } from 'assert'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'MyService', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.exceptionHandler(AssertionError, async (error) => { + return { error: { message: error.message, type: error.name } }; +}); + +app.onQuery('createSomething', async () => { + throw new AssertionError({ + message: 'This is an assertion Error', + }); +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponse.json b/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponse.json new file mode 100644 index 000000000..77c248e2f --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponse.json @@ -0,0 +1,23 @@ +{ + "data": { + "createSomething": null + }, + "errors": [ + { + "path": [ + "createSomething" + ], + "data": null, + "errorType": "AssertionError", + "errorInfo": null, + "locations": [ + { + "line": 2, + "column": 3, + "sourceName": null + } + ], + "message": "This is an assertion Error" + } + ] +} \ No newline at end of file diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponseMapping.vtl b/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponseMapping.vtl new file mode 100644 index 000000000..db3ee9f21 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponseMapping.vtl @@ -0,0 +1,5 @@ +#if (!$util.isNull($ctx.result.error)) + $util.error($ctx.result.error.message, $ctx.result.error.type) +#end + +$utils.toJson($ctx.result) \ No newline at end of file From fb08824b405b6724a8e54c0b92eea02118d1b29f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Aug 2025 21:44:24 +0600 Subject: [PATCH 28/33] fix: update exception handler logging to use error name for clarity --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index b1ea4714a..4ffc60532 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -225,12 +225,12 @@ class AppSyncGraphQLResolver extends Router { if (exceptionHandler) { try { this.logger.debug( - `Calling exception handler for error: ${error.constructor.name}` + `Calling exception handler for error: ${error.name}` ); return await exceptionHandler(error); } catch (handlerError) { this.logger.error( - `Exception handler for ${error.constructor.name} threw an error`, + `Exception handler for ${error.name} threw an error`, handlerError ); } From 433f10a0fac47d4a6ee329843fbd060516fbfe29 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Aug 2025 21:50:14 +0600 Subject: [PATCH 29/33] fix: improve error handling for AssertionError with detailed error structure --- .../src/appsync-graphql/Router.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 2de7a2c9f..38ce251fc 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -974,14 +974,18 @@ class Router { * // Register an exception handler for AssertionError * app.exceptionHandler(AssertionError, async (error) => { * return { - * message: 'Assertion failed', - * details: error.message + * error: { + * message: error.message, + * type: error.name + * } * }; * }); * * // Register a resolver that might throw an AssertionError - * app.onQuery('getTodo', async () => { - * throw new AssertionError(); + * app.onQuery('createSomething', async () => { + * throw new AssertionError({ + * message: 'This is an assertion Error', + * }); * }); * * export const handler = async (event, context) => @@ -1001,14 +1005,18 @@ class Router { * ⁣@app.exceptionHandler(AssertionError) * async handleAssertionError(error: AssertionError) { * return { - * message: 'Assertion failed', - * details: error.message + * error: { + * message: error.message, + * type: error.name + * } * }; * } * * ⁣@app.onQuery('getUser') * async getUser() { - * throw new AssertionError(); + * throw new AssertionError({ + * message: 'This is an assertion Error', + * }); * } * * async handler(event, context) { From 4952c795b55feea1a436285bff7a2554d10eb16a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Aug 2025 21:50:47 +0600 Subject: [PATCH 30/33] fix: update parameter name in exceptionHandler for clarity --- packages/event-handler/src/appsync-graphql/Router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 38ce251fc..9034ef0e7 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -1030,7 +1030,7 @@ class Router { * export const handler = lambda.handler.bind(lambda); * ``` * - * @param errorClass - The error class to handle. + * @param error - The error class to handle. * @param handler - The handler function to be called when the error is caught. */ public exceptionHandler( From 85b5c8c18138fb09660f0e6267b58c23c7855de7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 13 Aug 2025 10:22:11 +0600 Subject: [PATCH 31/33] fix: enhance exception handling documentation and response structure --- docs/features/event-handler/appsync-graphql.md | 6 +++--- .../event-handler/appsync-graphql/exceptionHandling.ts | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index 394b460dd..dd03c9c87 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -150,13 +150,13 @@ You can access the original Lambda event or context for additional information. ### Exception Handling -You can use the **`exceptionHandler`** method with any exception. This allows you to handle a common exception outside your resolver. +You can use the **`exceptionHandler`** method with any exception. This allows you to handle common errors outside your resolver and return a custom response. -When using exception handlers, you'll also need to configure your AppSync response mapping template to properly handle the custom error responses. +You can use a VTL response mapping template to detect these custom responses and forward them to the client gracefully. === "Exception Handling" - ```typescript hl_lines="11-13 16-18" + ```typescript hl_lines="11-18 21-23" --8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts" ``` diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts index 959bf3a57..7741e2f1c 100644 --- a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts @@ -9,7 +9,12 @@ const logger = new Logger({ const app = new AppSyncGraphQLResolver({ logger }); app.exceptionHandler(AssertionError, async (error) => { - return { error: { message: error.message, type: error.name } }; + return { + error: { + message: error.message, + type: error.name, + }, + }; }); app.onQuery('createSomething', async () => { From 5aaac53285c456dd21474d7d0202aa0be91a0608 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 13 Aug 2025 10:32:39 +0600 Subject: [PATCH 32/33] fix: clarify documentation for exception handler options and resolver error handling --- packages/event-handler/src/appsync-graphql/Router.ts | 4 ++-- packages/event-handler/src/types/appsync-graphql.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 9034ef0e7..c6a8c17fd 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -961,8 +961,8 @@ class Router { * Register an exception handler for a specific error class. * * Registers a handler for a specific error class that can be thrown by GraphQL resolvers. - * The handler will be invoked when an error of the specified class (or its subclasses) is thrown - * from any resolver function. + * The handler will be invoked when an error of the specified class is thrown from any + * resolver function. * * @example * ```ts diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 32d7b83dd..e7cac6b6c 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -205,7 +205,7 @@ type ErrorClass = new (...args: any[]) => T; */ type ExceptionHandlerOptions = { /** - * The error class/constructor to handle (must be Error or a subclass) + * The error class to handle (must be Error or a subclass) */ error: ErrorClass; /** From c79f5bc68482c6b25b3f8ed2372fe83a8bc1e94c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 13 Aug 2025 10:35:50 +0600 Subject: [PATCH 33/33] test: enhance test descriptions for clarity in exception handling scenarios --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 2a665ae7f..f6573231a 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -851,7 +851,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should prefer exact error class match over inheritance match', async () => { + it('should prefer exact error class match over inheritance match during exception handling', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -982,7 +982,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should not interfere with ResolverNotFoundException', async () => { + it('should not interfere with ResolverNotFoundException during exception handling', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1003,7 +1003,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ).rejects.toThrow('No resolver found for Query-nonExistentResolver'); }); - it('should work as a method decorator', async () => { + it('should work as a method decorator for `exceptionHandler`', async () => { // Prepare const app = new AppSyncGraphQLResolver();