Skip to content

Commit d0f44b8

Browse files
committed
Rework createAsyncThunk error handling behavior
- Removed `finished` action - Serialized `Error` objects to a plain object - Ensured errors in `fulfilled` dispatches won't get caught wrongly - Changed to re-throw errors in case the user wants to handle them
1 parent 9d57d62 commit d0f44b8

File tree

3 files changed

+56
-39
lines changed

3 files changed

+56
-39
lines changed

src/createAsyncThunk.test.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createAsyncThunk } from './createAsyncThunk'
1+
import { createAsyncThunk, miniSerializeError } from './createAsyncThunk'
22
import { configureStore } from './configureStore'
33

44
describe('createAsyncThunk', () => {
@@ -7,7 +7,6 @@ describe('createAsyncThunk', () => {
77

88
expect(thunkActionCreator.fulfilled.type).toBe('testType/fulfilled')
99
expect(thunkActionCreator.pending.type).toBe('testType/pending')
10-
expect(thunkActionCreator.finished.type).toBe('testType/finished')
1110
expect(thunkActionCreator.rejected.type).toBe('testType/rejected')
1211
})
1312

@@ -29,7 +28,7 @@ describe('createAsyncThunk', () => {
2928

3029
await store.dispatch(thunkActionCreator())
3130

32-
expect(timesReducerCalled).toBe(3)
31+
expect(timesReducerCalled).toBe(2)
3332
})
3433

3534
it('accepts arguments and dispatches the actions on resolve', async () => {
@@ -65,11 +64,6 @@ describe('createAsyncThunk', () => {
6564
2,
6665
thunkActionCreator.fulfilled(result, generatedRequestId, args)
6766
)
68-
69-
expect(dispatch).toHaveBeenNthCalledWith(
70-
3,
71-
thunkActionCreator.finished(generatedRequestId, args)
72-
)
7367
})
7468

7569
it('accepts arguments and dispatches the actions on reject', async () => {
@@ -90,21 +84,23 @@ describe('createAsyncThunk', () => {
9084

9185
const thunkFunction = thunkActionCreator(args)
9286

93-
await thunkFunction(dispatch, undefined, undefined)
87+
try {
88+
await thunkFunction(dispatch, undefined, undefined)
89+
} catch (e) {}
9490

9591
expect(dispatch).toHaveBeenNthCalledWith(
9692
1,
9793
thunkActionCreator.pending(generatedRequestId, args)
9894
)
9995

100-
expect(dispatch).toHaveBeenNthCalledWith(
101-
2,
102-
thunkActionCreator.rejected(error, generatedRequestId, args)
103-
)
96+
expect(dispatch).toHaveBeenCalledTimes(2)
10497

105-
expect(dispatch).toHaveBeenNthCalledWith(
106-
3,
107-
thunkActionCreator.finished(generatedRequestId, args)
108-
)
98+
console.log(dispatch.mock.calls)
99+
100+
// Have to check the bits of the action separately since the error was processed
101+
const errorAction = dispatch.mock.calls[1][0]
102+
expect(errorAction.error).toEqual(miniSerializeError(error))
103+
expect(errorAction.meta.requestId).toBe(generatedRequestId)
104+
expect(errorAction.meta.args).toBe(args)
109105
})
110106
})

src/createAsyncThunk.ts

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,36 @@ type AsyncThunksArgs<S, E, D extends Dispatch = Dispatch> = {
99
requestId: string
1010
}
1111

12+
interface SimpleError {
13+
name?: string
14+
message?: string
15+
stack?: string
16+
code?: string
17+
}
18+
19+
const commonProperties: (keyof SimpleError)[] = [
20+
'name',
21+
'message',
22+
'stack',
23+
'code'
24+
]
25+
26+
// Reworked from https://github.com/sindresorhus/serialize-error
27+
export const miniSerializeError = (value: any): any => {
28+
if (typeof value === 'object' && value !== null) {
29+
const simpleError: SimpleError = {}
30+
for (const property of commonProperties) {
31+
if (typeof value[property] === 'string') {
32+
simpleError[property] = value[property]
33+
}
34+
}
35+
36+
return simpleError
37+
}
38+
39+
return value
40+
}
41+
1242
/**
1343
*
1444
* @param type
@@ -52,16 +82,6 @@ export function createAsyncThunk<
5282
}
5383
)
5484

55-
const finished = createAction(
56-
type + '/finished',
57-
(requestId: string, args: ActionParams) => {
58-
return {
59-
payload: undefined,
60-
meta: { args, requestId }
61-
}
62-
}
63-
)
64-
6585
const rejected = createAction(
6686
type + '/rejected',
6787
(error: Error, requestId: string, args: ActionParams) => {
@@ -81,32 +101,34 @@ export function createAsyncThunk<
81101
) => {
82102
const requestId = nanoid()
83103

104+
let result: Returned
84105
try {
85106
dispatch(pending(requestId, args))
86-
// TODO Also ugly types
87-
const result = (await payloadCreator(args, {
107+
108+
result = (await payloadCreator(args, {
88109
dispatch,
89110
getState,
90111
extra,
91112
requestId
92113
} as TA)) as Returned
93-
94-
// TODO How do we avoid errors in here from hitting the catch clause?
95-
return dispatch(fulfilled(result, requestId, args))
96114
} catch (err) {
97-
// TODO Errors aren't serializable
98-
dispatch(rejected(err, requestId, args))
99-
} finally {
100-
// TODO IS there really a benefit from a "finished" action?
101-
dispatch(finished(requestId, args))
115+
const serializedError = miniSerializeError(err)
116+
dispatch(rejected(serializedError, requestId, args))
117+
// Rethrow this so the user can handle if desired
118+
throw err
102119
}
120+
121+
// We dispatch "success" _after_ the catch, to avoid having any errors
122+
// here get swallowed by the try/catch block,
123+
// per https://twitter.com/dan_abramov/status/770914221638942720
124+
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
125+
return dispatch(fulfilled(result!, requestId, args))
103126
}
104127
}
105128

106129
actionCreator.pending = pending
107130
actionCreator.rejected = rejected
108131
actionCreator.fulfilled = fulfilled
109-
actionCreator.finished = finished
110132

111133
return actionCreator
112134
}

type-tests/files/createAsyncThunk.typetest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,5 @@ function fn() {}
2222
expectType<number>(action.payload)
2323
})
2424
.addCase(async.rejected, (_, action) => {})
25-
.addCase(async.finished, (_, action) => {})
2625
)
2726
}

0 commit comments

Comments
 (0)