Skip to content

Commit

Permalink
refactor(action-listener-middleware): add async error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
FaberVitale committed Nov 13, 2021
1 parent 24bbf9f commit f040ad6
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/action-listener-middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Current options are:

- `extra`: an optional "extra argument" that will be injected into the `listenerApi` parameter of each listener. Equivalent to [the "extra argument" in the Redux Thunk middleware](https://redux.js.org/usage/writing-logic-thunks#injecting-config-values-into-thunks).

- `onError`: an optional error handler that gets called with synchronous errors raised by `listener` and `predicate`.
- `onError`: an optional error handler that gets called with synchronous and async errors raised by `listener` and synchronous errors thrown by `predicate`.

### `listenerMiddleware.addListener(predicate, listener, options?) : Unsubscribe`

Expand Down
71 changes: 62 additions & 9 deletions packages/action-listener-middleware/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,49 @@ export interface ActionListenerMiddlewareAPI<S, D extends Dispatch<AnyAction>>
extra: unknown
}

export type SyncActionListener<
A extends AnyAction,
S,
D extends Dispatch<AnyAction>
> = (action: A, api: ActionListenerMiddlewareAPI<S, D>) => void

export type AsyncActionListener<
A extends AnyAction,
S,
D extends Dispatch<AnyAction>
> = (action: A, api: ActionListenerMiddlewareAPI<S, D>) => Promise<void>

/**
* @alpha
*/
export type ActionListener<
A extends AnyAction,
S,
D extends Dispatch<AnyAction>
> = (action: A, api: ActionListenerMiddlewareAPI<S, D>) => void
> = SyncActionListener<A, S, D> | AsyncActionListener<A, S, D>

/**
* Additional information regarding the error.
*/
export interface ListenerErrorInfo {
async: boolean
/**
* Which function has generated the exception.
*/
raisedBy: 'listener' | 'predicate'
/**
* When the function that has raised the error has been called.
*/
phase: MiddlewarePhase
}

/**
* Gets notified with synchronous and asynchronous errors raised by `listeners` or `predicates`.
* @param error The thrown error.
* @param errorInfo Additional information regarding the thrown error.
*/
export interface ListenerErrorHandler {
(error: unknown): void
(error: unknown, errorInfo: ListenerErrorInfo): void
}

export interface ActionListenerOptions {
Expand All @@ -123,7 +155,7 @@ export interface ActionListenerOptions {
export interface CreateListenerMiddlewareOptions<ExtraArgument = unknown> {
extra?: ExtraArgument
/**
* Receives synchronous errors that are raised by `listener` and `listenerOption.predicate`.
* Receives synchronous and asynchronous errors that are raised by `listener` and `listenerOption.predicate`.
*/
onError?: ListenerErrorHandler
}
Expand Down Expand Up @@ -310,10 +342,11 @@ export type ActionListenerMiddleware<
*/
const safelyNotifyError = (
errorHandler: ListenerErrorHandler,
errorToNotify: unknown
errorToNotify: unknown,
errorInfo: ListenerErrorInfo
): void => {
try {
errorHandler(errorToNotify)
errorHandler(errorToNotify, errorInfo)
} catch (errorHandlerError) {
// We cannot let an error raised here block the listener queue.
// The error raised here will be picked up by `window.onerror`, `process.on('error')` etc...
Expand Down Expand Up @@ -456,7 +489,11 @@ export function createActionListenerMiddleware<
try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
safelyNotifyError(onError, predicateError)
safelyNotifyError(onError, predicateError, {
async: false,
raisedBy: 'predicate',
phase: currentPhase,
})
runListener = false
}
}
Expand All @@ -466,7 +503,7 @@ export function createActionListenerMiddleware<
}

try {
entry.listener(action, {
let promiseLikeOrUndefined = entry.listener(action, {
...api,
getOriginalState,
// eslint-disable-next-line @typescript-eslint/no-use-before-define
Expand All @@ -478,8 +515,24 @@ export function createActionListenerMiddleware<
listenerMap.set(entry.id, entry)
},
})
} catch (listenerError) {
safelyNotifyError(onError, listenerError)

if (promiseLikeOrUndefined) {
Promise.resolve(promiseLikeOrUndefined).catch(
(asyncListenerError) => {
safelyNotifyError(onError, asyncListenerError, {
async: true,
raisedBy: 'listener',
phase: currentPhase,
})
}
)
}
} catch (syncListenerError) {
safelyNotifyError(onError, syncListenerError, {
async: false,
raisedBy: 'listener',
phase: currentPhase,
})
}
}
if (currentPhase === 'beforeReducer') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ describe('createActionListenerMiddleware', () => {
])
})

test('Notifies listener errors to `onError`, if provided', () => {
test('Notifies sync listener errors to `onError`, if provided', () => {
const onError = jest.fn()
middleware = createActionListenerMiddleware({
onError,
Expand All @@ -645,7 +645,43 @@ describe('createActionListenerMiddleware', () => {
})

store.dispatch(testAction1('a'))
expect(onError).toBeCalledWith(listenerError)
expect(onError).toBeCalledWith(listenerError, {
async: false,
raisedBy: 'listener',
phase: 'afterReducer',
})
})

test('Notifies async listeners errors to `onError`, if provided', async () => {
const onError = jest.fn()
middleware = createActionListenerMiddleware({
onError,
})
reducer = jest.fn(() => ({}))
store = configureStore({
reducer,
middleware: (gDM) => gDM().prepend(middleware),
})

const listenerError = new Error('Boom!')
const matcher = (action: any): action is any => true

middleware.addListener({
matcher,
listener: async () => {
throw listenerError
},
})

store.dispatch(testAction1('a'))

await Promise.resolve()

expect(onError).toBeCalledWith(listenerError, {
async: true,
raisedBy: 'listener',
phase: 'afterReducer',
})
})

test('condition method resolves promise when the predicate succeeds', async () => {
Expand Down

0 comments on commit f040ad6

Please sign in to comment.