diff --git a/docs/features/parser.md b/docs/features/parser.md index 5acfb145c2..96c3addd3e 100644 --- a/docs/features/parser.md +++ b/docs/features/parser.md @@ -265,6 +265,33 @@ If the parsing is successful, the `data` field will contain the parsed event, ot 3. Use `error` to handle the error message 4. Use `originalEvent` to get the original event and recover +## Handling parse errors inline + +If you want to intercept a parsing failure and return a custom response instead of throwing, use the `errorHandler` option. +The callback receives the `ParseError` and the original, unparsed event, and must return synchronously - `errorHandler` does not support `async` functions, and providing one is a type error. + +If `errorHandler` returns a value, it is used as the response and the error is not thrown. If it returns `undefined`, the original error is rethrown. + +`errorHandler` and `safeParse` are mutually exclusive: with `safeParse` the parser never throws, so `errorHandler` would never be invoked. + +=== "Middy.js middleware" + ```typescript hl_lines="25-29" + --8<-- "examples/snippets/parser/errorHandlerMiddy.ts" + ``` + + 1. Use `errorHandler` to intercept a `ParseError` and return a custom response + 2. Use `error` and `event` to log the failure and the original, unparsed event + 3. Returning a value short-circuits the parse failure with that value + +=== "Decorator" + ```typescript hl_lines="27-31" + --8<-- "examples/snippets/parser/errorHandlerDecorator.ts" + ``` + + 1. Use `errorHandler` to intercept a `ParseError` and return a custom response + 2. Use `error` and `event` to log the failure and the original, unparsed event + 3. Returning a value short-circuits the parse failure with that value + ## Manual parsing You can use built-in envelopes and schemas to parse the incoming events manually, without using middy or decorator. diff --git a/examples/snippets/parser/errorHandlerDecorator.ts b/examples/snippets/parser/errorHandlerDecorator.ts new file mode 100644 index 0000000000..2a4c3d293a --- /dev/null +++ b/examples/snippets/parser/errorHandlerDecorator.ts @@ -0,0 +1,41 @@ +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { parser } from '@aws-lambda-powertools/parser'; +import type { Context } from 'aws-lambda'; +import { z } from 'zod'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ + schema: orderSchema, + errorHandler: (error, event) => { + // (1)! + logger.error('Failed to parse event', { error, event }); // (2)! + return { statusCode: 400, body: 'Invalid order payload' }; // (3)! + }, + }) + public async handler(event: Order, _context: Context): Promise { + for (const item of event.items) { + logger.info('Processing item', { item }); + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/examples/snippets/parser/errorHandlerMiddy.ts b/examples/snippets/parser/errorHandlerMiddy.ts new file mode 100644 index 0000000000..f2bdffe6fb --- /dev/null +++ b/examples/snippets/parser/errorHandlerMiddy.ts @@ -0,0 +1,36 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import middy from '@middy/core'; +import { z } from 'zod'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +export const handler = middy() + .use( + parser({ + schema: orderSchema, + errorHandler: (error, event) => { + // (1)! + logger.error('Failed to parse event', { error, event }); // (2)! + return { statusCode: 400, body: 'Invalid order payload' }; // (3)! + }, + }) + ) + .handler(async (event): Promise => { + for (const item of event.items) { + logger.info('Processing item', { item }); + } + }); diff --git a/packages/parser/src/errorHandler.ts b/packages/parser/src/errorHandler.ts new file mode 100644 index 0000000000..7001995a80 --- /dev/null +++ b/packages/parser/src/errorHandler.ts @@ -0,0 +1,40 @@ +import { ParseError } from './errors.js'; +import type { ErrorHandler } from './types/parser.js'; + +/** + * Sentinel returned by {@link invokeErrorHandler} when the error should be rethrown, either because + * no errorHandler was provided, the error wasn't a {@link ParseError}, or the errorHandler itself + * returned `undefined`. + */ +const NO_RECOVERY = Symbol('NO_RECOVERY'); + +/** + * Invoke the customer-provided errorHandler, if any, for a parse failure. + * + * Returns {@link NO_RECOVERY} when the caller should rethrow the original error, or the + * errorHandler's return value when it should be used to recover instead. + */ +function invokeErrorHandler( + errorHandler: ErrorHandler | undefined, + error: unknown, + event: unknown +): TErrorHandlerReturn | typeof NO_RECOVERY { + if (!errorHandler || !(error instanceof ParseError)) { + return NO_RECOVERY; + } + + const result = errorHandler(error, event); + if ( + typeof result === 'object' && + result !== null && + typeof (result as { then?: unknown }).then === 'function' + ) { + throw new TypeError( + 'errorHandler must return synchronously; async errorHandler functions are not supported' + ); + } + + return result ?? NO_RECOVERY; +} + +export { invokeErrorHandler, NO_RECOVERY }; diff --git a/packages/parser/src/middleware/index.ts b/packages/parser/src/middleware/index.ts index 064e47c855..63dab676e9 100644 --- a/packages/parser/src/middleware/index.ts +++ b/packages/parser/src/middleware/index.ts @@ -1,6 +1,7 @@ import type { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; import type { MiddlewareObj } from '@middy/core'; import type { StandardSchemaV1 } from '@standard-schema/spec'; +import { invokeErrorHandler, NO_RECOVERY } from '../errorHandler.js'; import { parse } from '../parser.js'; import type { Envelope } from '../types/envelope.js'; import type { ParserOptions, ParserOutput } from '../types/parser.js'; @@ -35,17 +36,48 @@ const parser = < TSchema extends StandardSchemaV1, TEnvelope extends Envelope, TSafeParse extends boolean = false, + TErrorHandlerReturn = never, >( - options: ParserOptions -): MiddlewareObj> => { + options: ParserOptions +): MiddlewareObj< + ParserOutput, + | ParserOutput + | Exclude +> => { + // Tracks which requests failed in *this* middleware's own `before` phase, so `onError` + // only recovers this parser's own parse failures - not an unrelated ParseError thrown + // by the handler or another parser middleware further down the chain. + const failedRequests = new WeakSet(); + const before = (request: MiddyLikeRequest): void => { const { schema, envelope, safeParse } = options; - request.event = parse(request.event, envelope, schema, safeParse); + try { + request.event = parse(request.event, envelope, schema, safeParse); + } catch (error) { + failedRequests.add(request); + throw error; + } + }; + + const onError = (request: MiddyLikeRequest): void => { + if (!failedRequests.has(request)) { + return; + } + + const result = invokeErrorHandler( + options.errorHandler, + request.error, + request.event + ); + if (result !== NO_RECOVERY) { + request.response = result; + } }; return { before, + onError, }; }; diff --git a/packages/parser/src/parserDecorator.ts b/packages/parser/src/parserDecorator.ts index d0166a4518..7d02789af8 100644 --- a/packages/parser/src/parserDecorator.ts +++ b/packages/parser/src/parserDecorator.ts @@ -1,6 +1,7 @@ import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { Callback, Context, Handler } from 'aws-lambda'; +import { invokeErrorHandler, NO_RECOVERY } from './errorHandler.js'; import { parse } from './parser.js'; import type { Envelope, ParserOptions } from './types/index.js'; import type { ParserOutput } from './types/parser.js'; @@ -66,14 +67,38 @@ import type { ParserOutput } from './types/parser.js'; * } * ``` * + * You can also provide an `errorHandler` callback to intercept parse errors and return a custom response + * instead of throwing. If the handler returns `undefined`, the error is rethrown. + * + * @example + * ```typescript + * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import { z } from 'zod'; + * import { parser } from '@aws-lambda-powertools/parser'; + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * }); + * + * class Lambda implements LambdaInterface { + * + * @parser({ schema: Order, errorHandler: (error) => ({ statusCode: 400, body: error.message }) }) + * public async handler(event: z.infer, _context: Context): Promise { + * return processOrder(event); + * } + * } + * ``` + * * @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not */ export const parser = < TSchema extends StandardSchemaV1, TEnvelope extends Envelope = undefined, TSafeParse extends boolean = false, + TErrorHandlerReturn = never, >( - options: ParserOptions + options: ParserOptions ): HandlerMethodDecorator => { return ( _target: unknown, @@ -82,13 +107,29 @@ export const parser = < ) => { const original = descriptor.value; - const { schema, envelope, safeParse } = options; + const { schema, envelope, safeParse, errorHandler } = options; descriptor.value = function ( this: Handler, ...args: [ParserOutput, Context, Callback] ) { - const parsedEvent = parse(args[0], envelope, schema, safeParse); + let parsedEvent: ParserOutput; + try { + parsedEvent = parse( + args[0], + envelope, + schema, + safeParse + ) as ParserOutput; + } catch (error) { + // Only a failure of this parser's own `parse()` call goes through errorHandler - a + // ParseError thrown by the wrapped handler body itself must propagate unchanged. + const result = invokeErrorHandler(errorHandler, error, args[0]); + if (result !== NO_RECOVERY) { + return result; + } + throw error; + } return original.apply(this, [parsedEvent, ...args.slice(1)]); }; diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts index 438bdbc7f6..c60f677bd5 100644 --- a/packages/parser/src/types/parser.ts +++ b/packages/parser/src/types/parser.ts @@ -1,4 +1,5 @@ import type { StandardSchemaV1 } from '@standard-schema/spec'; +import type { ParseError } from '../errors.js'; import type { ArrayEnvelope, DynamoDBArrayEnvelope, @@ -7,18 +8,42 @@ import type { } from './envelope.js'; /** - * Options for the parser used in middy middleware and decorator + * A callback invoked when parsing fails with a {@link ParseError}, receiving the error and the original, + * unparsed event. Returning a value short-circuits the parse failure with that value; returning `undefined` + * rethrows the error. The callback must be synchronous - returning a `Promise` is a type error. */ -type ParserOptions< +type ErrorHandler = ( + error: ParseError, + event: unknown +) => TErrorHandlerReturn extends Promise ? never : TErrorHandlerReturn; + +type ParserOptionsBase< TSchema extends StandardSchemaV1, TEnvelope extends Envelope, - TSafeParse extends boolean, > = { schema: TSchema; envelope?: TEnvelope; - safeParse?: TSafeParse; }; +/** + * Options for the parser used in middy middleware and decorator. + * + * `safeParse` and `errorHandler` are mutually exclusive: with `safeParse` the parser never throws, + * so an `errorHandler` would never be invoked. + */ +type ParserOptions< + TSchema extends StandardSchemaV1, + TEnvelope extends Envelope, + TSafeParse extends boolean, + TErrorHandlerReturn = unknown, +> = ParserOptionsBase & + (TSafeParse extends true + ? { safeParse: TSafeParse; errorHandler?: never } + : { + safeParse?: TSafeParse; + errorHandler?: ErrorHandler; + }); + /** * A successful parsing result with the parsed data when using safeParse */ @@ -133,6 +158,7 @@ type InferOutput = NonNullable< >['output']; export type { + ErrorHandler, InferOutput, ParsedResult, ParsedResultError, diff --git a/packages/parser/tests/types/parser.test-d.ts b/packages/parser/tests/types/parser.test-d.ts index 3cc9ec3a95..20aff668b4 100644 --- a/packages/parser/tests/types/parser.test-d.ts +++ b/packages/parser/tests/types/parser.test-d.ts @@ -3,6 +3,7 @@ import { describe, expectTypeOf, it } from 'vitest'; import { z } from 'zod'; import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; import { SqsEnvelope } from '../../src/envelopes/sqs.js'; +import type { ParseError } from '../../src/errors.js'; import { JSONStringified } from '../../src/helpers/index.js'; import { parser } from '../../src/middleware/index.js'; import { parse } from '../../src/parser.js'; @@ -77,4 +78,25 @@ describe('Parser types', () => { expectTypeOf(event).toEqualTypeOf(); }); }); + + it('rejects errorHandler when safeParse is true', () => { + middy().use( + parser({ + schema: userSchema, + safeParse: true, + // @ts-expect-error - safeParse and errorHandler are mutually exclusive + errorHandler: (_error: ParseError, _event: unknown) => undefined, + }) + ); + }); + + it('rejects an async errorHandler', () => { + middy().use( + parser({ + schema: userSchema, + // @ts-expect-error - errorHandler must be synchronous + errorHandler: async (_error: ParseError, _event: unknown) => undefined, + }) + ); + }); }); diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts index 067755b9e4..be7badc42d 100644 --- a/packages/parser/tests/unit/parser.decorator.test.ts +++ b/packages/parser/tests/unit/parser.decorator.test.ts @@ -1,6 +1,6 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import type { Context } from 'aws-lambda'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { z } from 'zod'; import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; import { ParseError } from '../../src/errors.js'; @@ -27,6 +27,10 @@ describe('Decorator: parser', () => { filename: 'base', }); + const nonParseErrorHandlerSpy = vi + .fn() + .mockReturnValue({ errorHandled: true }); + class TestClass implements LambdaInterface { @parser({ schema: extendedSchema }) public async handler(event: event, _context: Context): Promise { @@ -64,6 +68,73 @@ describe('Decorator: parser', () => { return event; } + @parser({ + schema, + errorHandler: (_error) => undefined, + }) + public async handlerWithErrorHandlerReturningUndefined( + event: z.infer, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema, + errorHandler: (error) => ({ errorHandled: true, message: error.message }), + }) + public async handlerWithErrorHandler( + event: z.infer, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema, + envelope: EventBridgeEnvelope, + errorHandler: (error) => ({ errorHandled: true, message: error.message }), + }) + public async handlerWithEnvelopeAndErrorHandler( + event: z.infer, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema, + errorHandler: (error, event) => ({ error, event }), + }) + public async handlerWithErrorHandlerReceivingEvent( + event: z.infer, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema, + errorHandler: nonParseErrorHandlerSpy, + }) + public async handlerThatThrowsNonParseError( + _event: z.infer, + _context: Context + ): Promise { + throw new Error('handler failure'); + } + + @parser({ + schema, + errorHandler: nonParseErrorHandlerSpy, + }) + public async handlerThatThrowsParseError( + _event: z.infer, + _context: Context + ): Promise { + throw new ParseError('unrelated failure'); + } + private anotherMethod(event: unknown): unknown { return event; } @@ -142,4 +213,137 @@ describe('Decorator: parser', () => { originalEvent: { foo: 'bar' }, }); }); + + it('rethrows the error when errorHandler returns undefined', () => { + // Act & Assess + expect(() => + lambda.handlerWithErrorHandlerReturningUndefined( + { foo: 'bar' } as unknown as z.infer, + {} as Context + ) + ).toThrow(ParseError); + }); + + it('calls the errorHandler when schema validation fails', async () => { + // Act + const result = await lambda.handlerWithErrorHandler( + { foo: 'bar' } as unknown as z.infer, + {} as Context + ); + + // Assess + expect(result).toEqual({ + errorHandled: true, + message: expect.any(String), + }); + }); + + it('does not call the errorHandler when schema validation succeeds', async () => { + // Prepare + const event = { name: 'John', age: 30 }; + + // Act + const result = await lambda.handlerWithErrorHandler( + event as unknown as z.infer, + {} as Context + ); + + // Assess + expect(result).toEqual(event); + }); + + it('rethrows the error when no errorHandler is provided and schema validation fails', () => { + // Act & Assess + expect(() => + lambda.handler({ foo: 'bar' } as unknown as event, {} as Context) + ).toThrow(ParseError); + }); + + it('calls the errorHandler when schema/envelope validation fails with an envelope', async () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = await lambda.handlerWithEnvelopeAndErrorHandler( + event as unknown as z.infer, + {} as Context + ); + + // Assess + expect(result).toEqual({ + errorHandled: true, + message: expect.any(String), + }); + }); + + it('passes the original event to the errorHandler', async () => { + // Prepare + const invalidEvent = { foo: 'bar' }; + + // Act + const result = await lambda.handlerWithErrorHandlerReceivingEvent( + invalidEvent as unknown as z.infer, + {} as Context + ); + + // Assess + expect(result).toEqual({ + error: expect.any(ParseError), + event: invalidEvent, + }); + }); + + it('does not call the errorHandler when the handler throws a non-ParseError', async () => { + // Prepare + nonParseErrorHandlerSpy.mockClear(); + + // Act & Assess + await expect( + lambda.handlerThatThrowsNonParseError( + payload as unknown as z.infer, + {} as Context + ) + ).rejects.toThrow('handler failure'); + expect(nonParseErrorHandlerSpy).not.toHaveBeenCalled(); + }); + + it('does not call the errorHandler when the handler itself throws a ParseError', async () => { + // Prepare + nonParseErrorHandlerSpy.mockClear(); + + // Act & Assess + await expect( + lambda.handlerThatThrowsParseError( + payload as unknown as z.infer, + {} as Context + ) + ).rejects.toThrow('unrelated failure'); + expect(nonParseErrorHandlerSpy).not.toHaveBeenCalled(); + }); + + it('throws a TypeError when the errorHandler returns a Promise', async () => { + // Prepare + class AsyncErrorHandlerClass implements LambdaInterface { + @parser({ + schema, + // @ts-expect-error - errorHandler must be synchronous + errorHandler: async (_error: ParseError) => ({ errorHandled: true }), + }) + public async handler( + event: z.infer, + _context: Context + ): Promise { + return event; + } + } + const asyncLambda = new AsyncErrorHandlerClass(); + + // Act & Assess + expect(() => + asyncLambda.handler( + { foo: 'bar' } as unknown as z.infer, + {} as Context + ) + ).toThrow(TypeError); + }); }); diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts index c138a3e5cf..52af5b52f2 100644 --- a/packages/parser/tests/unit/parser.middy.test.ts +++ b/packages/parser/tests/unit/parser.middy.test.ts @@ -1,6 +1,6 @@ import middy from '@middy/core'; import type { Context } from 'aws-lambda'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { z } from 'zod'; import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; import { SqsEnvelope } from '../../src/envelopes/sqs.js'; @@ -190,4 +190,203 @@ describe('Middleware: parser', () => { originalEvent: event, }); }); + + it('rethrows the error when errorHandler returns undefined', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act & Assess + await expect( + middy() + .use( + parser({ + schema: z.number(), + errorHandler: (_error) => undefined, + }) + ) + .handler((event) => event)(event as unknown as number, {} as Context) + ).rejects.toThrow(ParseError); + }); + + it('calls the errorHandler and short-circuits when schema validation fails', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act + const result = await middy() + .use( + parser({ + schema: z.number(), + errorHandler: (error) => ({ + errorHandled: true, + message: error.message, + }), + }) + ) + .handler((event) => event)(event as unknown as number, {} as Context); + + // Assess + expect(result).toEqual({ + errorHandled: true, + message: expect.any(String), + }); + }); + + it('does not call the errorHandler when schema validation succeeds', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act + const result = await middy() + .use( + parser({ + schema: schema, + errorHandler: (error) => ({ + errorHandled: true, + message: error.message, + }), + }) + ) + .handler((event) => event)( + event as unknown as z.infer, + {} as Context + ); + + // Assess + expect(result).toEqual(event); + }); + + it('rethrows the error when no errorHandler is provided and schema validation fails', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act & Assess + await expect( + middy() + .use(parser({ schema: z.number() })) + .handler((event) => event)(event as unknown as number, {} as Context) + ).rejects.toThrow(ParseError); + }); + + it('calls the errorHandler when schema validation fails with an envelope', async () => { + // Prepare + const event = structuredClone(baseEventBridgeEvent); + + // Act + const result = await middy() + .use( + parser({ + schema: schema, + envelope: EventBridgeEnvelope, + errorHandler: (error) => ({ + errorHandled: true, + message: error.message, + }), + }) + ) + .handler((event) => event)( + event as unknown as z.infer, + {} as Context + ); + + // Assess + expect(result).toEqual({ + errorHandled: true, + message: expect.any(String), + }); + }); + + it('passes the original event to the errorHandler', async () => { + // Prepare + const event = structuredClone(JSONPayload); + const errorHandler = vi.fn().mockReturnValue({ errorHandled: true }); + + // Act + await middy() + .use(parser({ schema: z.number(), errorHandler })) + .handler((event) => event)(event as unknown as number, {} as Context); + + // Assess + expect(errorHandler).toHaveBeenCalledWith(expect.any(ParseError), event); + }); + + it('does not call the errorHandler when the handler throws a non-ParseError', async () => { + // Prepare + const event = structuredClone(JSONPayload); + const errorHandler = vi.fn(); + + // Act & Assess + await expect( + middy() + .use(parser({ schema: schema, errorHandler })) + .handler(() => { + throw new Error('handler failure'); + })(event as unknown as z.infer, {} as Context) + ).rejects.toThrow('handler failure'); + expect(errorHandler).not.toHaveBeenCalled(); + }); + + it('does not call the errorHandler when the handler itself throws a ParseError', async () => { + // Prepare + const event = structuredClone(JSONPayload); + const errorHandler = vi.fn(); + + // Act & Assess + await expect( + middy() + .use(parser({ schema: schema, errorHandler })) + .handler(() => { + throw new ParseError('unrelated failure'); + })(event as unknown as z.infer, {} as Context) + ).rejects.toThrow('unrelated failure'); + expect(errorHandler).not.toHaveBeenCalled(); + }); + + it('throws a TypeError when the errorHandler returns a Promise', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act & Assess + await expect( + middy() + .use( + parser({ + schema: z.number(), + // @ts-expect-error - errorHandler must be synchronous + errorHandler: async (_error) => ({ errorHandled: true }), + }) + ) + .handler((event) => event)(event as unknown as number, {} as Context) + ).rejects.toThrow(TypeError); + }); + + it('still runs other middlewares onError when the errorHandler recovers', async () => { + // Prepare + const event = structuredClone(JSONPayload); + const otherOnError = vi.fn(); + + // Act + // Note: middy runs onError middlewares in reverse attachment order, so `otherOnError` + // (attached before parser) runs *after* parser's onError - it must still run even though + // parser's onError already recovered from the error. + const result = await middy() + .use({ onError: otherOnError }) + .use( + parser({ + schema: z.number(), + errorHandler: (error) => ({ + errorHandled: true, + message: error.message, + }), + }) + ) + .handler((event) => event)(event as unknown as number, {} as Context); + + // Assess + expect(result).toEqual({ + errorHandled: true, + message: expect.any(String), + }); + expect(otherOnError).toHaveBeenCalledTimes(1); + }); });