From 9f97957fada263d94d68b40f2147b79c48a4d937 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 3 Aug 2025 12:15:56 +0600 Subject: [PATCH 01/45] 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 91d81a4a13..e8ff9b18a6 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/45] 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 e8ff9b18a6..672088851c 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/45] 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 0000000000..1ff4e50307 --- /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 672088851c..dc9e48bd3d 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/45] 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 f6a0428e60..b1ea4714a2 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 57a243650b..f20d39a2f6 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/45] 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 0000000000..f9c1258c7f --- /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/45] 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 95cbcc25f8..bcb1cc440c 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/45] 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 f9c1258c7f..59923dfaa0 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/45] 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 6dcd7dca26..5c5b20301f 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/45] 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 f20d39a2f6..482926b7f5 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/45] 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 482926b7f5..2de7a2c9fa 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/45] 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 bcb1cc440c..557850dda3 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/45] 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 557850dda3..baf8783a74 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/45] 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 baf8783a74..9de4412f92 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/45] 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 9de4412f92..ee480568cf 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/45] 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 ee480568cf..f126442a6f 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/45] 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 f126442a6f..bc3b82111f 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/45] 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 bc3b82111f..bd57a632b9 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/45] 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 b1ea4714a2..8677efc9f3 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 bd57a632b9..ce0b60c82b 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/45] 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 ce0b60c82b..3964bc91c4 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/45] 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 8677efc9f3..b1ea4714a2 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 1ff4e50307..4029e1be50 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 3964bc91c4..4971861294 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 59923dfaa0..68ea0359b4 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/45] 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 4029e1be50..714e377728 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/45] 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 dc9e48bd3d..32d7b83dd7 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/45] 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 4971861294..bac204863e 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/45] 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 bac204863e..8182947175 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/45] 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 8182947175..2a665ae7f5 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/45] 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 68ea0359b4..2672221b21 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/45] 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 29485aa412..984bd10f63 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 e782593fba..394b460dd7 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 0000000000..959bf3a574 --- /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 0000000000..77c248e2f3 --- /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 0000000000..db3ee9f21d --- /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/45] 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 b1ea4714a2..4ffc60532b 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/45] 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 2de7a2c9fa..38ce251fc7 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/45] 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 38ce251fc7..9034ef0e79 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/45] 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 394b460dd7..dd03c9c87a 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 959bf3a574..7741e2f1c6 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/45] 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 9034ef0e79..c6a8c17fd8 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 32d7b83dd7..e7cac6b6cd 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/45] 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 2a665ae7f5..f6573231a7 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(); From 9366f861776eb09b51fa71516a46f98f8a626e00 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 21 Aug 2025 10:28:26 +0600 Subject: [PATCH 34/45] feat: PR feedback resolved --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 24 +- .../ExceptionHandlerRegistry.ts | 43 ++- .../src/appsync-graphql/Router.ts | 6 +- .../src/types/appsync-graphql.ts | 2 +- .../AppSyncGraphQLResolver.test.ts | 266 ++++++++++++++++++ .../ExceptionHandlerRegistry.test.ts | 178 +++++++++++- 6 files changed, 492 insertions(+), 27 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 4ffc60532b..cf4e71fdfb 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -166,7 +166,8 @@ class AppSyncGraphQLResolver extends Router { } return this.#withErrorHandling( () => this.#executeBatchResolvers(event, context, options), - event[0] + event[0], + options ); } if (!isAppSyncGraphQLEvent(event)) { @@ -178,7 +179,8 @@ class AppSyncGraphQLResolver extends Router { return this.#withErrorHandling( () => this.#executeSingleResolver(event, context, options), - event + event, + options ); } @@ -189,17 +191,20 @@ class AppSyncGraphQLResolver extends Router { * * @param fn - A function returning a Promise to be executed with error handling. * @param event - The AppSync resolver event (single or first of batch). + * @param options - Optional resolve options for customizing resolver behavior. */ async #withErrorHandling( fn: () => Promise, - event: AppSyncResolverEvent> + event: AppSyncResolverEvent>, + options?: ResolveOptions ): Promise { try { return await fn(); } catch (error) { return await this.#handleError( error, - `An error occurred in handler ${event.info.fieldName}` + `An error occurred in handler ${event.info.fieldName}`, + options ); } } @@ -214,20 +219,25 @@ class AppSyncGraphQLResolver extends Router { * * @param error - The error object to handle. * @param errorMessage - A descriptive message to log alongside the error. + * @param options - Optional resolve options for customizing resolver behavior. * @throws InvalidBatchResponseException | ResolverNotFoundException */ - async #handleError(error: unknown, errorMessage: string): Promise { + async #handleError( + error: unknown, + errorMessage: string, + options?: ResolveOptions + ): 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) { + if (error instanceof Error) { const exceptionHandler = this.exceptionHandlerRegistry.resolve(error); if (exceptionHandler) { try { this.logger.debug( `Calling exception handler for error: ${error.name}` ); - return await exceptionHandler(error); + return await exceptionHandler.apply(options?.scope ?? this, [error]); } catch (handlerError) { this.logger.error( `Exception handler for ${error.name} threw an error`, diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts index 714e377728..85638aed2c 100644 --- a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -1,5 +1,6 @@ import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import type { + ErrorClass, ExceptionHandler, ExceptionHandlerOptions, ExceptionHandlerRegistryOptions, @@ -23,15 +24,34 @@ class ExceptionHandlerRegistry { } /** - * Registers an exception handler for a specific error class. + * Registers an exception handler for one or more error classes. * * 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. + * @param options - The options containing the error class(es) and their associated handler. + * @param options.error - A single error class or an array of error classes to handle. + * @param options.handler - The exception handler function that will be invoked when the error occurs. */ public register(options: ExceptionHandlerOptions): void { const { error, handler } = options; - const errorName = error.name; + const errors = Array.isArray(error) ? error : [error]; + + for (const err of errors) { + this.registerErrorHandler(err, handler as ExceptionHandler); + } + } + + /** + * Registers a error handler for a specific error class. + * + * @param errorClass - The error class to register the handler for. + * @param handler - The exception handler function. + */ + private registerErrorHandler( + errorClass: ErrorClass, + handler: ExceptionHandler + ): void { + const errorName = errorClass.name; this.#logger.debug(`Adding exception handler for error class ${errorName}`); @@ -42,8 +62,8 @@ class ExceptionHandlerRegistry { } this.handlers.set(errorName, { - error, - handler: handler as ExceptionHandler, + error: errorClass, + handler, }); } @@ -51,11 +71,11 @@ 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 class name. - * If a matching handler is found, it is returned; otherwise, `undefined` is returned. + * If a matching handler is found, it is returned; otherwise, `null` is returned. * * @param error - The error instance for which to resolve an exception handler. */ - public resolve(error: Error): ExceptionHandler | undefined { + public resolve(error: Error): ExceptionHandler | null { const errorName = error.name; this.#logger.debug(`Looking for exception handler for error: ${errorName}`); @@ -66,14 +86,7 @@ class ExceptionHandlerRegistry { } 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; + return null; } } diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index c6a8c17fd8..29745e2663 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -1034,14 +1034,14 @@ class Router { * @param handler - The handler function to be called when the error is caught. */ public exceptionHandler( - error: ErrorClass, + error: ErrorClass | ErrorClass[], handler: ExceptionHandler ): void; public exceptionHandler( - error: ErrorClass + error: ErrorClass | ErrorClass[] ): MethodDecorator; public exceptionHandler( - error: ErrorClass, + error: ErrorClass | ErrorClass[], handler?: ExceptionHandler ): MethodDecorator | undefined { if (typeof handler === 'function') { diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index e7cac6b6cd..98002419f1 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -207,7 +207,7 @@ type ExceptionHandlerOptions = { /** * The error class to handle (must be Error or a subclass) */ - error: ErrorClass; + error: ErrorClass | ErrorClass[]; /** * The handler function to be called when the error is caught */ 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 f6573231a7..08e27a7d09 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1008,12 +1008,15 @@ describe('Class: AppSyncGraphQLResolver', () => { const app = new AppSyncGraphQLResolver(); class Lambda { + public readonly scope = 'scoped'; + @app.exceptionHandler(ValidationError) async handleValidationError(error: ValidationError) { return { message: 'Decorator validation failed', details: error.message, type: 'decorator_validation_error', + scope: this.scope, }; } @@ -1023,6 +1026,7 @@ describe('Class: AppSyncGraphQLResolver', () => { message: 'Decorator user not found', details: error.message, type: 'decorator_user_not_found', + scope: this.scope, }; } @@ -1062,12 +1066,274 @@ describe('Class: AppSyncGraphQLResolver', () => { message: 'Decorator validation failed', details: 'Decorator error test', type: 'decorator_validation_error', + scope: 'scoped', }); expect(notFoundError).toEqual({ message: 'Decorator user not found', details: 'User with ID 0 not found', type: 'decorator_user_not_found', + scope: 'scoped', + }); + }); + + it('should handle array of error classes with single exception handler function', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + app.exceptionHandler([ValidationError, NotFoundError], async (error) => { + return { + message: 'User service error', + details: error.message, + type: 'user_service_error', + errorClass: error.name, + }; + }); + + app.onQuery<{ id: string }>('getId', async ({ id }) => { + if (!id) { + throw new ValidationError('User ID is required for retrieval'); + } + if (id === 'missing') { + throw new NotFoundError('Requested user does not exist'); + } + if (id === 'database-error') { + throw new DatabaseError('Database connection timeout'); + } + return { id, name: 'Retrieved User' }; + }); + + // Act + const validationResult = await app.resolve( + onGraphqlEventFactory('getId', 'Query', {}), + context + ); + const notFoundResult = await app.resolve( + onGraphqlEventFactory('getId', 'Query', { id: 'missing' }), + context + ); + const databaseErrorResult = await app.resolve( + onGraphqlEventFactory('getId', 'Query', { id: 'database-error' }), + context + ); + + // Assess + expect(console.debug).toHaveBeenCalledWith( + 'Adding exception handler for error class ValidationError' + ); + expect(console.debug).toHaveBeenCalledWith( + 'Adding exception handler for error class NotFoundError' + ); + expect(validationResult).toEqual({ + message: 'User service error', + details: 'User ID is required for retrieval', + type: 'user_service_error', + errorClass: 'ValidationError', }); + expect(notFoundResult).toEqual({ + message: 'User service error', + details: 'Requested user does not exist', + type: 'user_service_error', + errorClass: 'NotFoundError', + }); + expect(databaseErrorResult).toEqual({ + error: 'DatabaseError - Database connection timeout', + }); + }); + + it('should preserve scope when using array error handler as method decorator', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + class OrderServiceLambda { + public readonly serviceName = 'OrderService'; + + @app.exceptionHandler([ValidationError, NotFoundError]) + async handleOrderErrors(error: ValidationError | NotFoundError) { + return { + message: `${this.serviceName} encountered an error`, + details: error.message, + type: 'order_service_error', + errorClass: error.name, + service: this.serviceName, + }; + } + + @app.onQuery('getOrder') + async getOrderById({ orderId }: { orderId: string }) { + if (!orderId) { + throw new ValidationError('Order ID is required'); + } + if (orderId === 'order-404') { + throw new NotFoundError('Order not found in system'); + } + if (orderId === 'db-error') { + throw new DatabaseError('Database unavailable'); + } + return { orderId, status: 'found', service: this.serviceName }; + } + + async handler(event: unknown, context: Context) { + return app.resolve(event, context, { + scope: this, + }); + } + } + + const orderServiceLambda = new OrderServiceLambda(); + const orderHandler = orderServiceLambda.handler.bind(orderServiceLambda); + + // Act + const validationResult = await orderHandler( + onGraphqlEventFactory('getOrder', 'Query', {}), + context + ); + const notFoundResult = await orderHandler( + onGraphqlEventFactory('getOrder', 'Query', { orderId: 'order-404' }), + context + ); + const databaseErrorResult = await orderHandler( + onGraphqlEventFactory('getOrder', 'Query', { orderId: 'db-error' }), + context + ); + const successResult = await orderHandler( + onGraphqlEventFactory('getOrder', 'Query', { orderId: 'order-123' }), + context + ); + + // Assess + expect(validationResult).toEqual({ + message: 'OrderService encountered an error', + details: 'Order ID is required', + type: 'order_service_error', + errorClass: 'ValidationError', + service: 'OrderService', + }); + expect(notFoundResult).toEqual({ + message: 'OrderService encountered an error', + details: 'Order not found in system', + type: 'order_service_error', + errorClass: 'NotFoundError', + service: 'OrderService', + }); + expect(successResult).toEqual({ + orderId: 'order-123', + status: 'found', + service: 'OrderService', + }); + expect(databaseErrorResult).toEqual({ + error: 'DatabaseError - Database unavailable', + }); + }); + + it('should handle mix of single and array error handlers with proper precedence', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.exceptionHandler([ValidationError, TypeError], async (error) => { + return { + message: 'Payment validation error', + details: error.message, + type: 'payment_validation_error', + errorClass: error.name, + }; + }); + + app.exceptionHandler(ValidationError, async (error) => { + return { + message: 'Specific payment validation error', + details: error.message, + type: 'specific_payment_validation_error', + errorClass: error.name, + }; + }); + + app.onQuery<{ amount: number; currency: string }>( + 'getPayment', + async ({ amount, currency }) => { + if (!amount || amount <= 0) { + throw new ValidationError('Invalid payment amount'); + } + if (!currency) { + throw new TypeError('Currency type is required'); + } + if (currency === 'INVALID') { + throw new RangeError('Unsupported currency'); + } + return { amount, currency, status: 'validated' }; + } + ); + + // Act + const validationResult = await app.resolve( + onGraphqlEventFactory('getPayment', 'Query', { + amount: 0, + currency: 'USD', + }), + context + ); + const typeErrorResult = await app.resolve( + onGraphqlEventFactory('getPayment', 'Query', { amount: 100 }), + context + ); + const rangeErrorResult = await app.resolve( + onGraphqlEventFactory('getPayment', 'Query', { + amount: 100, + currency: 'INVALID', + }), + context + ); + + // Assess + expect(validationResult).toEqual({ + message: 'Specific payment validation error', + details: 'Invalid payment amount', + type: 'specific_payment_validation_error', + errorClass: 'ValidationError', + }); + expect(typeErrorResult).toEqual({ + message: 'Payment validation error', + details: 'Currency type is required', + type: 'payment_validation_error', + errorClass: 'TypeError', + }); + expect(rangeErrorResult).toEqual({ + error: 'RangeError - Unsupported currency', + }); + }); + + it('should handle empty array of error classes gracefully', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + app.exceptionHandler([], async (error) => { + return { + message: 'This should never be called', + details: error.message, + }; + }); + + app.onQuery<{ requestId: string }>('getId', async ({ requestId }) => { + if (requestId === 'validation-error') { + throw new ValidationError('Invalid request format'); + } + return { requestId, status: 'processed' }; + }); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getId', 'Query', { + requestId: 'validation-error', + }), + context + ); + + // Assess + expect(result).toEqual({ + error: 'ValidationError - Invalid request format', + }); + expect(console.debug).not.toHaveBeenCalledWith( + expect.stringContaining('Adding exception handler for error class') + ); }); // #endregion Exception handling 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 2672221b21..9912254179 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/ExceptionHandlerRegistry.test.ts @@ -80,6 +80,182 @@ describe('Class: ExceptionHandlerRegistry', () => { const resolved = registry.resolve(new OtherError('fail')); // Assess - expect(resolved).toBeUndefined(); + expect(resolved).toBeNull(); + }); + + it('registers an exception handler for multiple error classes using an array', () => { + // Prepare + class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } + } + class AuthenticationError extends Error { + constructor(message: string) { + super(message); + this.name = 'AuthenticationError'; + } + } + const handler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ + error: [ValidationError, AuthenticationError], + handler, + }); + + // Assess + expect(registry.handlers.size).toBe(2); + expect(registry.handlers.get('ValidationError')).toEqual({ + error: ValidationError, + handler, + }); + expect(registry.handlers.get('AuthenticationError')).toEqual({ + error: AuthenticationError, + handler, + }); + }); + + it('registers different handlers for different error arrays', () => { + // Prepare + class DatabaseError extends Error { + constructor(message: string) { + super(message); + this.name = 'DatabaseError'; + } + } + class ConnectionError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConnectionError'; + } + } + class UIError extends Error { + constructor(message: string) { + super(message); + this.name = 'UIError'; + } + } + const backendHandler = vi.fn(); + const frontendHandler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ + error: [DatabaseError, ConnectionError], + handler: backendHandler, + }); + registry.register({ + error: [UIError], + handler: frontendHandler, + }); + + // Assess + expect(registry.handlers.size).toBe(3); + expect(registry.resolve(new DatabaseError('DB failed'))).toBe( + backendHandler + ); + expect(registry.resolve(new ConnectionError('Connection failed'))).toBe( + backendHandler + ); + expect(registry.resolve(new UIError('UI failed'))).toBe(frontendHandler); + }); + + it('logs warnings and replaces handlers when error classes in array are already registered', () => { + // Prepare + class ConflictError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConflictError'; + } + } + class DuplicateError extends Error { + constructor(message: string) { + super(message); + this.name = 'DuplicateError'; + } + } + const originalHandler = vi.fn(); + const newHandler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: ConflictError, handler: originalHandler }); + registry.register({ + error: [ConflictError, DuplicateError], + handler: newHandler, + }); + + // Assess + expect(registry.handlers.size).toBe(2); + expect(registry.handlers.get('ConflictError')).toEqual({ + error: ConflictError, + handler: newHandler, + }); + expect(registry.handlers.get('DuplicateError')).toEqual({ + error: DuplicateError, + handler: newHandler, + }); + expect(console.warn).toHaveBeenCalledWith( + "An exception handler for error class 'ConflictError' is already registered. The previous handler will be replaced." + ); + }); + + it('handles mixed registration of single errors and error arrays', () => { + // Prepare + class SingleError extends Error { + constructor(message: string) { + super(message); + this.name = 'SingleError'; + } + } + class ArrayError1 extends Error { + constructor(message: string) { + super(message); + this.name = 'ArrayError1'; + } + } + class ArrayError2 extends Error { + constructor(message: string) { + super(message); + this.name = 'ArrayError2'; + } + } + const singleHandler = vi.fn(); + const arrayHandler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: SingleError, handler: singleHandler }); + registry.register({ + error: [ArrayError1, ArrayError2], + handler: arrayHandler, + }); + + // Assess + expect(registry.handlers.size).toBe(3); + expect(registry.resolve(new SingleError('Single error'))).toBe( + singleHandler + ); + expect(registry.resolve(new ArrayError1('Array error 1'))).toBe( + arrayHandler + ); + expect(registry.resolve(new ArrayError2('Array error 2'))).toBe( + arrayHandler + ); + }); + + it('handles empty array of errors gracefully', () => { + // Prepare + const handler = vi.fn(); + const registry = getRegistry(); + + // Act + registry.register({ error: [], handler }); + + // Assess + expect(registry.handlers.size).toBe(0); }); }); From 10adbb92927bf182c8a779ed07b7d1c9bd2a834b Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 21 Aug 2025 11:06:31 +0600 Subject: [PATCH 35/45] fix: enhance exception handling documentation and add example resolver --- docs/features/event-handler/appsync-graphql.md | 12 ++++++++++-- .../appsync-graphql/exceptionHandlingResolver.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 examples/snippets/event-handler/appsync-graphql/exceptionHandlingResolver.js diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index dd03c9c87a..f99e40e5e0 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -150,9 +150,11 @@ 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 common errors outside your resolver and return a custom response. +You can use the **`exceptionHandler`** method to handle any exception. This allows you to handle common errors outside your resolver and return a custom response. -You can use a VTL response mapping template to detect these custom responses and forward them to the client gracefully. +The **`exceptionHandler`** method also supports passing an array of exceptions that you wish to handle with a single handler. + +You can use an AppSync JavaScript resolver or a VTL response mapping template to detect these custom responses and forward them to the client gracefully. === "Exception Handling" @@ -160,6 +162,12 @@ You can use a VTL response mapping template to detect these custom responses and --8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts" ``` +=== "APPSYNC JS Resolver" + + ```js hl_lines="11-13" + --8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandlingResolver.js" + ``` + === "VTL Response Mapping Template" ```velocity hl_lines="1-3" diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResolver.js b/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResolver.js new file mode 100644 index 0000000000..ceed023c26 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandlingResolver.js @@ -0,0 +1,15 @@ +import { util } from '@aws-appsync/utils'; + +export function request(ctx) { + return { + operation: 'Invoke', + payload: ctx, + }; +} + +export function response(ctx) { + if (ctx.result.error) { + return util.error(ctx.result.error.message, ctx.result.error.type); + } + return ctx.result; +} From ddb217b32ab3f94d260b034ac4bc061bb1e277d9 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 21 Aug 2025 11:08:34 +0600 Subject: [PATCH 36/45] fix: revert Node.js version to 22.13.1 in Dockerfile --- docs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Dockerfile b/docs/Dockerfile index 984bd10f63..29485aa412 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.15.1-r0 npm +RUN apk add --no-cache nodejs=22.13.1-r0 npm COPY requirements.txt /tmp/ RUN pip install --require-hashes -r /tmp/requirements.txt From be3dc59bb9a779fef95175877fe1944680532bf6 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 21 Aug 2025 11:42:51 +0600 Subject: [PATCH 37/45] fix: sonarqube issue resolved --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 3 ++- 1 file changed, 2 insertions(+), 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 08e27a7d09..5a820845a0 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1173,9 +1173,10 @@ describe('Class: AppSyncGraphQLResolver', () => { } async handler(event: unknown, context: Context) { - return app.resolve(event, context, { + const resolved = app.resolve(event, context, { scope: this, }); + return resolved; } } From 8a02d3da6fe67199271dc7c4cf0b5c123ab13e8d Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:06:12 +0600 Subject: [PATCH 38/45] Update docs/features/event-handler/appsync-graphql.md Co-authored-by: Andrea Amorosi --- docs/features/event-handler/appsync-graphql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index f99e40e5e0..9b41fceda3 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -152,7 +152,7 @@ You can access the original Lambda event or context for additional information. You can use the **`exceptionHandler`** method to handle any exception. This allows you to handle common errors outside your resolver and return a custom response. -The **`exceptionHandler`** method also supports passing an array of exceptions that you wish to handle with a single handler. +The `exceptionHandler` method also supports passing an array of exceptions that you wish to handle with a single handler. You can use an AppSync JavaScript resolver or a VTL response mapping template to detect these custom responses and forward them to the client gracefully. From d7463c599079d86c7197fa0bc856016fc810a9c0 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:06:19 +0600 Subject: [PATCH 39/45] Update examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts Co-authored-by: Andrea Amorosi --- .../snippets/event-handler/appsync-graphql/exceptionHandling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts index 7741e2f1c6..6267bfcf4c 100644 --- a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts @@ -1,6 +1,6 @@ import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; import { Logger } from '@aws-lambda-powertools/logger'; -import { AssertionError } from 'assert'; +import { AssertionError } from 'node:assert'; import type { Context } from 'aws-lambda'; const logger = new Logger({ From 2600bde36d02e3e3e4da0eb11a10960c64ec9884 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:06:46 +0600 Subject: [PATCH 40/45] Update examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts Co-authored-by: Andrea Amorosi --- .../snippets/event-handler/appsync-graphql/exceptionHandling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts index 6267bfcf4c..fc354527e5 100644 --- a/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts +++ b/examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts @@ -19,7 +19,7 @@ app.exceptionHandler(AssertionError, async (error) => { app.onQuery('createSomething', async () => { throw new AssertionError({ - message: 'This is an assertion Error', + message: 'This is an assertion error', }); }); From b2073ca33c0c342d0afafe73b465fe26bb172d45 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:07:03 +0600 Subject: [PATCH 41/45] Update packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts Co-authored-by: Andrea Amorosi --- .../event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index cf4e71fdfb..a338ef9c5b 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -201,7 +201,7 @@ class AppSyncGraphQLResolver extends Router { try { return await fn(); } catch (error) { - return await this.#handleError( + return this.#handleError( error, `An error occurred in handler ${event.info.fieldName}`, options From 7bb58823e39b1555915dc7b4cfab34b67ce872a0 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:07:11 +0600 Subject: [PATCH 42/45] Update packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts Co-authored-by: Andrea Amorosi --- .../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 5a820845a0..c9708952c8 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1,5 +1,5 @@ import context from '@aws-lambda-powertools/testing-utils/context'; -import { AssertionError } from 'assert'; +import { AssertionError } from 'node:assert'; import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js'; From 92e49528a90f54b614fbbf334d105e9b739dafe8 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:07:21 +0600 Subject: [PATCH 43/45] Update packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts Co-authored-by: Andrea Amorosi --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 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 c9708952c8..801d4cabaf 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -771,7 +771,10 @@ describe('Class: AppSyncGraphQLResolver', () => { // Prepare const app = new AppSyncGraphQLResolver(); - app.exceptionHandler(errorClass, async (err) => { + app.exceptionHandler(errorClass, async (err) => ({ + message, + errorName: err.constructor.name, + }); return { message, errorName: err.constructor.name, From 8b1b1e2a4597f79e33f5639f029fce94ca576e81 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 27 Aug 2025 10:08:32 +0600 Subject: [PATCH 44/45] Update docs/features/event-handler/appsync-graphql.md Co-authored-by: Andrea Amorosi --- docs/features/event-handler/appsync-graphql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index 9b41fceda3..3cae9ed869 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -150,7 +150,7 @@ You can access the original Lambda event or context for additional information. ### Exception Handling -You can use the **`exceptionHandler`** method to handle any exception. This allows you to handle common errors outside your resolver and return a custom response. +You can use the `exceptionHandler` method to handle any exception. This allows you to handle common errors outside your resolver and return a custom response. The `exceptionHandler` method also supports passing an array of exceptions that you wish to handle with a single handler. From dd69bfb0caa41ac5757c73600b247b3927275aab Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 27 Aug 2025 11:10:18 +0600 Subject: [PATCH 45/45] fix: PR feedback handled --- .../ExceptionHandlerRegistry.ts | 2 +- .../src/types/appsync-graphql.ts | 1 + .../AppSyncGraphQLResolver.test.ts | 37 ++++++++----------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts index 85638aed2c..261c426b8d 100644 --- a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -37,7 +37,7 @@ class ExceptionHandlerRegistry { const errors = Array.isArray(error) ? error : [error]; for (const err of errors) { - this.registerErrorHandler(err, handler as ExceptionHandler); + this.registerErrorHandler(err, handler); } } diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 98002419f1..cd4f8685ed 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -1,5 +1,6 @@ import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import type { AppSyncResolverEvent, Context } from 'aws-lambda'; +import type { ExceptionHandlerRegistry } from '../appsync-graphql/ExceptionHandlerRegistry.js'; import type { RouteHandlerRegistry } from '../appsync-graphql/RouteHandlerRegistry.js'; import type { Router } from '../appsync-graphql/Router.js'; 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 801d4cabaf..06b653fcd9 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1,5 +1,5 @@ -import context from '@aws-lambda-powertools/testing-utils/context'; import { AssertionError } from 'node:assert'; +import context from '@aws-lambda-powertools/testing-utils/context'; import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js'; @@ -760,7 +760,7 @@ describe('Class: AppSyncGraphQLResolver', () => { message: 'Aggregation failed', }, ])( - 'should invoke exception handler for %s', + 'invokes exception handler for %s', async ({ errorClass, message, @@ -772,14 +772,9 @@ describe('Class: AppSyncGraphQLResolver', () => { const app = new AppSyncGraphQLResolver(); app.exceptionHandler(errorClass, async (err) => ({ - message, - errorName: err.constructor.name, - }); - return { - message, - errorName: err.constructor.name, - }; - }); + message, + errorName: err.constructor.name, + })); app.onQuery('getUser', async () => { throw errorClass === AggregateError @@ -801,7 +796,7 @@ describe('Class: AppSyncGraphQLResolver', () => { } ); - it('should handle multiple different error types with specific exception handlers', async () => { + it('handles multiple different error types with specific exception handlers', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -854,7 +849,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should prefer exact error class match over inheritance match during exception handling', async () => { + it('prefers exact error class match over inheritance match during exception handling', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -892,7 +887,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should fall back to default error formatting when no exception handler is found', async () => { + it('falls back to default error formatting when no exception handler is found', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -920,7 +915,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should fall back to default error formatting when exception handler throws an error', async () => { + it('falls 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'); @@ -955,7 +950,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ); }); - it('should invoke sync exception handlers and return their result', async () => { + it('invokes sync exception handlers and return their result', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -985,7 +980,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should not interfere with ResolverNotFoundException during exception handling', async () => { + it('does not interfere with ResolverNotFoundException during exception handling', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1006,7 +1001,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ).rejects.toThrow('No resolver found for Query-nonExistentResolver'); }); - it('should work as a method decorator for `exceptionHandler`', async () => { + it('works as a method decorator for `exceptionHandler`', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1079,7 +1074,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should handle array of error classes with single exception handler function', async () => { + it('handles array of error classes with single exception handler function', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); @@ -1143,7 +1138,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should preserve scope when using array error handler as method decorator', async () => { + it('preserves scope when using array error handler as method decorator', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1229,7 +1224,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should handle mix of single and array error handlers with proper precedence', async () => { + it('handles mix of single and array error handlers with proper precedence', async () => { // Prepare const app = new AppSyncGraphQLResolver(); @@ -1305,7 +1300,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('should handle empty array of error classes gracefully', async () => { + it('handles empty array of error classes gracefully', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console });