Skip to content

Commit 6e66f4f

Browse files
authored
Merge pull request #4102 from JoshuaKGoldberg/safe-promises
Added 'SafePromise' branded Promises for createAsyncThunk
2 parents c3cf5a7 + ea73204 commit 6e66f4f

File tree

4 files changed

+34
-11
lines changed

4 files changed

+34
-11
lines changed

packages/toolkit/src/createAsyncThunk.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
Id,
1212
IsAny,
1313
IsUnknown,
14+
SafePromise,
1415
TypeGuard,
1516
} from './tsHelpers'
1617
import { nanoid } from './nanoid'
@@ -242,7 +243,7 @@ export type AsyncThunkAction<
242243
dispatch: GetDispatch<ThunkApiConfig>,
243244
getState: () => GetState<ThunkApiConfig>,
244245
extra: GetExtra<ThunkApiConfig>
245-
) => Promise<
246+
) => SafePromise<
246247
| ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>>
247248
| ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>
248249
> & {
@@ -676,7 +677,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
676677
}
677678
return finalAction
678679
})()
679-
return Object.assign(promise as Promise<any>, {
680+
return Object.assign(promise as SafePromise<any>, {
680681
abort,
681682
requestId,
682683
arg,

packages/toolkit/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,6 @@ export { combineSlices } from './combineSlices'
203203

204204
export type { WithSlice } from './combineSlices'
205205

206-
export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions } from './tsHelpers'
206+
export type { ExtractDispatchExtensions as TSHelpersExtractDispatchExtensions, SafePromise } from './tsHelpers'
207207

208208
export { formatProdErrorMessage } from './formatProdErrorMessage'

packages/toolkit/src/query/core/buildInitiate.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import type { QueryResultSelectorResult } from './buildSelectors'
2121
import type { Dispatch } from 'redux'
2222
import { isNotNullish } from '../utils/isNotNullish'
2323
import { countObjectKeys } from '../utils/countObjectKeys'
24+
import type { SafePromise } from '../../tsHelpers'
25+
import { asSafePromise } from '../../tsHelpers'
2426

2527
declare module './module' {
2628
export interface ApiEndpointQuery<
@@ -60,7 +62,7 @@ type StartQueryActionCreator<
6062

6163
export type QueryActionCreatorResult<
6264
D extends QueryDefinition<any, any, any, any>
63-
> = Promise<QueryResultSelectorResult<D>> & {
65+
> = SafePromise<QueryResultSelectorResult<D>> & {
6466
arg: QueryArgFrom<D>
6567
requestId: string
6668
subscriptionOptions: SubscriptionOptions | undefined
@@ -90,7 +92,7 @@ type StartMutationActionCreator<
9092

9193
export type MutationActionCreatorResult<
9294
D extends MutationDefinition<any, any, any, any>
93-
> = Promise<
95+
> = SafePromise<
9496
| { data: ResultTypeFrom<D> }
9597
| {
9698
error:
@@ -335,7 +337,7 @@ You must add the middleware for RTK-Query to function correctly!`
335337
const selectFromState = () => selector(getState())
336338

337339
const statePromise: QueryActionCreatorResult<any> = Object.assign(
338-
forceQueryFn
340+
(forceQueryFn
339341
? // a query has been forced (upsertQueryData)
340342
// -> we want to resolve it once data has been written with the data that will be written
341343
thunkResult.then(selectFromState)
@@ -345,7 +347,9 @@ You must add the middleware for RTK-Query to function correctly!`
345347
Promise.resolve(stateAfter)
346348
: // query just started or one is already in flight
347349
// -> wait for the running query, then resolve with data from after that
348-
Promise.all([runningQuery, thunkResult]).then(selectFromState),
350+
Promise.all([runningQuery, thunkResult]).then(
351+
selectFromState
352+
)) as SafePromise<any>,
349353
{
350354
arg,
351355
requestId,
@@ -421,10 +425,10 @@ You must add the middleware for RTK-Query to function correctly!`
421425
const thunkResult = dispatch(thunk)
422426
middlewareWarning(dispatch)
423427
const { requestId, abort, unwrap } = thunkResult
424-
const returnValuePromise = thunkResult
425-
.unwrap()
426-
.then((data) => ({ data }))
427-
.catch((error) => ({ error }))
428+
const returnValuePromise = asSafePromise(
429+
thunkResult.unwrap().then((data) => ({ data })),
430+
(error) => ({ error })
431+
)
428432

429433
const reset = () => {
430434
dispatch(removeMutationResult({ requestId, fixedCacheKey }))

packages/toolkit/src/tsHelpers.ts

+18
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,21 @@ export type Tail<T extends any[]> = T extends [any, ...infer Tail]
207207
: never
208208

209209
export type UnknownIfNonSpecific<T> = {} extends T ? unknown : T
210+
211+
/**
212+
* A Promise that will never reject.
213+
* @see https://github.com/reduxjs/redux-toolkit/issues/4101
214+
*/
215+
export type SafePromise<T> = Promise<T> & {
216+
__linterBrands: 'SafePromise'
217+
}
218+
219+
/**
220+
* Properly wraps a Promise as a {@link SafePromise} with .catch(fallback).
221+
*/
222+
export function asSafePromise<Resolved, Rejected>(
223+
promise: Promise<Resolved>,
224+
fallback: (error: unknown) => Rejected
225+
) {
226+
return promise.catch(fallback) as SafePromise<Resolved | Rejected>
227+
}

0 commit comments

Comments
 (0)