Skip to content

Commit

Permalink
feat: add support for custom exception handling
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Dec 30, 2022
1 parent 36cbc56 commit 3705b5f
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 9 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,52 @@ await middleware
assert.deepEqual(context.stack, ['fn1', 'final handler'])
```

### Error handler
By default, the exceptions raised in the middleware pipeline are bubbled upto the `run` method and you can capture them using `try/catch` block. Also, when an exception is raised, the middleware downstream logic will not run, unless middleware internally wraps the `next` method call inside `try/catch` block.

To simply the exception handling process, you can define a custom error handler to catch the exceptions and resume the downstream flow of middleware.

```ts
const context = {
stack: [],
}
const middleware = new Middleware()

middleware.add((ctx: typeof context, next: NextFn) => {
ctx.stack.push('middleware 1 upstream')
await next()
ctx.stack.push('middleware 1 downstream')
})

middleware.add((ctx: typeof context, next: NextFn) => {
ctx.stack.push('middleware 2 upstream')
throw new Error('Something went wrong')
})

middleware.add((ctx: typeof context, next: NextFn) => {
ctx.stack.push('middleware 3 upstream')
await next()
ctx.stack.push('middleware 3 downstream')
})

await middleware
.runner()
.errorHandler((error) => {
console.log(error)
context.stack.push('error handler')
})
.finalHandler(() => {
context.stack.push('final handler')
})
.run((fn, next) => fn(context, next))

assert.deepEqual(context.stack, [
'middleware 1 upstream',
'middleware 2 upstream',
'middleware 1 downstream'
])
```

[gh-workflow-image]: https://img.shields.io/github/workflow/status/poppinss/middleware/test?style=for-the-badge
[gh-workflow-url]: https://github.com/poppinss/middleware/actions/workflows/test.yml "Github action"

Expand Down
40 changes: 39 additions & 1 deletion src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* file that was distributed with this source code.
*/

import type { Executor, FinalHandler } from './types.js'
import type { ErrorHandler, Executor, FinalHandler } from './types.js'

/**
* Run a function only once. Tightly coupled with the Runner class
Expand Down Expand Up @@ -61,6 +61,11 @@ export class Runner<MiddlewareFn extends any> {
*/
#finalHandler: FinalHandler = DEFAULT_FINAL_HANDLER

/**
* Error handler to self handle errors
*/
#errorHandler?: ErrorHandler

constructor(middleware: MiddlewareFn[]) {
this.#middleware = middleware
}
Expand All @@ -85,6 +90,24 @@ export class Runner<MiddlewareFn extends any> {
return self.#executor(middleware, once(self, self.#invoke))
}

/**
* Same as invoke, but captures errors
*/
#invokeWithErrorManagement(self: Runner<MiddlewareFn>): Promise<void> | void {
const middleware = self.#middleware[self.#currentIndex++]

/**
* Empty stack
*/
if (!middleware) {
return self.#finalHandler().catch(self.#errorHandler)
}

return self
.#executor(middleware, once(self, self.#invokeWithErrorManagement))
.catch(self.#errorHandler)
}

/**
* Final handler to be executed, when the chain ends successfully.
*/
Expand All @@ -93,12 +116,27 @@ export class Runner<MiddlewareFn extends any> {
return this
}

/**
* Specify a custom error handler to use. Defining an error handler
* turns will make run method not throw an exception and instead
* run the upstream middleware logic
*/
errorHandler(errorHandler: ErrorHandler): this {
this.#errorHandler = errorHandler
return this
}

/**
* Start the middleware queue and pass params to it. The `params`
* array will be passed as spread arguments.
*/
async run(cb: Executor<MiddlewareFn>): Promise<void> {
this.#executor = cb

if (this.#errorHandler) {
return this.#invokeWithErrorManagement(this)
}

return this.#invoke(this)
}
}
15 changes: 12 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@
* file that was distributed with this source code.
*/

export type NextFn = () => Promise<void> | void
export type NextFn = () => Promise<any> | any

/**
* Final handler is called when the entire chain has been
* executed successfully.
*/
export type FinalHandler = () => Promise<void> | void
export type FinalHandler = () => Promise<any>

/**
* Error handler is called any method in the pipeline raises
* an exception
*/
export type ErrorHandler = (error: any) => Promise<any>

/**
* The executor function that invokes the middleware
*/
export type Executor<MiddlewareFn extends any> = (middleware: MiddlewareFn, next: NextFn) => any
export type Executor<MiddlewareFn extends any> = (
middleware: MiddlewareFn,
next: NextFn
) => Promise<any>
148 changes: 143 additions & 5 deletions tests/runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ test.group('Runner', () => {
},
])

await runner.run((fn, next) => fn.handle({}, next))
await runner.run(async (fn, next) => fn.handle({}, next))
assert.equal(chain[0], null)
assert.instanceOf(chain[1], Foo)
assert.equal(chain[0], null)
Expand Down Expand Up @@ -233,7 +233,7 @@ test.group('Runner', () => {

const runner = new Runner([first])

await runner.run((fn) => fn())
await runner.run(async (fn) => fn())
assert.deepEqual(chain, ['first'])
})

Expand Down Expand Up @@ -364,7 +364,7 @@ test.group('Runner', () => {
}

runner.finalHandler(() => finalHandler('foo'))
await runner.run((fn, next) => fn('foo', next))
await runner.run(async (fn, next) => fn('foo', next))

assert.deepEqual(stack, ['foo', 'foo'])
})
Expand All @@ -383,7 +383,7 @@ test.group('Runner', () => {
}

runner.finalHandler(() => finalHandler('bar'))
await runner.run((fn, next) => fn('bar', next))
await runner.run(async (fn, next) => fn('bar', next))

assert.deepEqual(stack, ['bar'])
})
Expand All @@ -405,7 +405,145 @@ test.group('Runner', () => {

runner.finalHandler(() => finalHandler('bar'))

await assert.rejects(() => runner.run((fn, next) => fn('bar', next)), 'Failed')
await assert.rejects(() => runner.run(async (fn, next) => fn('bar', next)), 'Failed')
assert.deepEqual(stack, ['bar'])
})

test('hand over exception a custom exception handler', async ({ assert }) => {
assert.plan(2)
const chain: string[] = []

async function first(_: any, next: NextFn) {
chain.push('first')
await next()
}

async function second() {
throw new Error('I am killed')
}

async function third(_: any, next: NextFn) {
chain.push('third')
await next()
}

const runner = new Runner([first, second, third])
runner.errorHandler(async (error) => {
assert.equal(error.message, 'I am killed')
})

await runner.run((fn, next) => fn({}, next))
assert.deepEqual(chain, ['first'])
})

test('execute middleware in reverse when error handler is defined', async ({ assert }) => {
const chain: string[] = []

async function first(_: any, next: NextFn) {
chain.push('first')
await next()
chain.push('first after')
}

async function second(_: any, next: NextFn) {
chain.push('second')
await sleep(200)
await next()
await sleep(100)
chain.push('second after')
}

async function third() {
throw new Error('Something went wrong')
}

const runner = new Runner([first, second, third])
runner.errorHandler(async () => {})

await runner.run((fn, next) => fn({}, next))
assert.deepEqual(chain, ['first', 'second', 'second after', 'first after'])
})

test('return error handler response via next method', async ({ assert }) => {
const chain: string[] = []

async function first(_: any, next: NextFn) {
chain.push('first')
const response = await next()
chain.push('first after')

return response
}

async function second(_: any, next: NextFn) {
chain.push('second')
await sleep(200)
const response = await next()
await sleep(100)
chain.push('second after')

return response
}

async function third() {
throw new Error('Something went wrong')
}

async function fourth() {
chain.push('fourth')
}

const runner = new Runner([first, second, third, fourth])
runner.errorHandler(async () => {
return 'handled'
})
runner.finalHandler(async () => {
chain.push('final handler')
})

const response = await runner.run((fn, next) => fn({}, next))
assert.equal(response, 'handled')
assert.deepEqual(chain, ['first', 'second', 'second after', 'first after'])
})

test('raise exception thrown by error handler', async ({ assert }) => {
const chain: string[] = []

async function first(_: any, next: NextFn) {
chain.push('first')
const response = await next()
chain.push('first after')

return response
}

async function second(_: any, next: NextFn) {
chain.push('second')
await sleep(200)
const response = await next()
await sleep(100)
chain.push('second after')

return response
}

async function third() {
throw new Error('Something went wrong')
}

async function fourth() {
chain.push('fourth')
}

const runner = new Runner([first, second, third, fourth])
runner.errorHandler(async (error) => {
throw error
})
runner.finalHandler(async () => {
chain.push('final handler')
})

await assert.rejects(() => runner.run((fn, next) => fn({}, next)), 'Something went wrong')
assert.deepEqual(chain, ['first', 'second'])
})
})

0 comments on commit 3705b5f

Please sign in to comment.