Repository: reduxjs/redux-toolkit
Pull request #361 (2020-02-15 16:37:42)

createAsyncThunk return fulfilled/rejected action instead of re-…
This is an alternative suggestion to #359.

I've created [a codesandbox](https://codesandbox.io/s/still-pond-7208i) that references the current alpha and implements a small data-fetching workflow.
The problem that becomes apparent is that, if there is an error within fetch and that error is not caught from dispatch, CRA in Dev Mode displays the red overlay of "something went terribly wrong":
![image](https://user-images.githubusercontent.com/4282439/74591497-dfa82f80-5018-11ea-883e-bca2f0fc02c2.png)

This happens during an event handler as well as during `useEffect`.

In production, this would probably not happen, but realisticly, devs would react to this by wrapping every `dispatch` call in an async function with try/catch or append a `.catch()` to it.
Doing the first thing is quite complicated, because you can't just change an effect to an async function (because that would return a promise), so you come up with something like 
```
  React.useEffect(() => {
    async function doFetch() {
      try {
        dispatch(fetchDataThunk({}));
      } catch {
        // do nothing
      }
    }
    if (state === "initial") {
      doFetch();
    }
  }, [state, dispatch]);
```
for something that would otherwise just have been
```
  React.useEffect(() => {
    if (state === "initial") {
        dispatch(fetchDataThunk({}));
    }
  }, [state, dispatch]);
```

So this makes stuff *much*  more complicated - also, event handlers that would just have been `() => dispatch(fetchDataThunk({}))` now become either `() => dispatch(fetchDataThunk({})).catch()` or `async () => { try {dispatch(fetchDataThunk({}))} catch {} }`.

What I want to say: avoiding this gets really messy.

So this is my suggestion of another solution:
The thunk would not return the promise return value in this case, but the "last dispatched action", which means either the `fulfilled` or the `rejected` action. That way, the error *can* be accessed from React, but not everyone is forced to handle it correctly.

netlify[bot]: Deploy preview for *redux-starter-kit-docs* ready!

Built with commit fa97974b93bc4fc8c3a6c7c2805b46bfa55b5296

https://deploy-preview-361--redux-starter-kit-docs.netlify.com
markerikson: Yeah, a user was alpha-testing `createAsyncThunk` last night and pointed this out, and it's exactly what I was also wondering might be a point of concern.

I've semi-frequently seen folks try to chain off thunks in components, but they've generally only cared about the success case.  

Having to check whether the promise return value is a failure action is awkward, but probably the best alternative here.
markerikson: hah, you forgot to update the public API file :)  I've only done that FOR EVERY ATTEMPT TO PUBLISH AN ALPHA SO FAR!
phryneas: Yeah, it's not perfect, but I think it can still be written pretty compact using the `.match` method:
`dispatch(async(3)).then(result => async.fulfilled.match(result) && /* do something */console.log(result))`
We'll just have to document that.

We could even add a helper that would re-introduce a `throw` for people that want it, so
`dispatch(async(3)).then(async.throwErrorsFromPromise).then(/*...*).catch(/*...*/)`
could be a possibility.
phryneas: > hah, you forgot to update the public API file :) I've only done that FOR EVERY ATTEMPT TO PUBLISH AN ALPHA SO FAR!

Yap :D
I even created it and force-pushed an amended commit where I forgot to add the change before amending -.-
markerikson: Sure, wanna put that throw helper in real fast?
phryneas: Yap, why not :)
codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/reduxjs/redux-toolkit/pr/361/builds/11078) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 5f7afe9cbfded74f7a67a23469a5c78eecab2968:

|Sandbox| Source |
|--|--|
|[bold-keldysh-1xehn](https://codesandbox.io/s/bold-keldysh-1xehn)| Configuration |
|[boring-leakey-jyicd](https://codesandbox.io/s/boring-leakey-jyicd)| Configuration |
|[angry-davinci-2h7jb](https://codesandbox.io/s/angry-davinci-2h7jb)| Configuration |

phryneas: Okay, it's in there. If you have a better idea, feel free to change that method name :D
markerikson: How about `rejectIfError`?

````js
dispatch(fetchStuff())
    .then(rejectIfError)
    .catch(handleError)
```

etc
phryneas: > How about `rejectIfError`?
> 
> ```js
> dispatch(fetchStuff())
>     .then(rejectIfError)
>     .catch(handleError)
> ```
> 
> etc

Hmm. Both names don't really reflect the other side of it, that the return value would also be unwrapped. :thinking: 

`unwrapAndRejectIfError` would reflect what it really does, but is a bit long.. `splitPromise` makes sense if you know what it does... `normalizePromise` likewise, but from the alternatives I can come up I think it's the best-sounding.... not really sure there. :man_shrugging: 

Patch:
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 700363d4..92233aa9 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -102,8 +102,13 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
 export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
 
 // @alpha (undocumented)
-export function createAsyncThunk<ActionType extends string, Returned, ActionParams = void, TA extends AsyncThunksArgs<any, any, any> = AsyncThunksArgs<unknown, unknown, Dispatch>>(type: ActionType, payloadCreator: (args: ActionParams, thunkArgs: TA) => Promise<Returned> | Returned): {
-    (args: ActionParams): (dispatch: TA["dispatch"], getState: TA["getState"], extra: TA["extra"]) => Promise<any>;
+export function createAsyncThunk<ActionType extends string, Returned, ActionParams = void, TA extends AsyncThunksArgs<any, any, any> = AsyncThunksArgs<unknown, unknown, Dispatch>>(type: ActionType, payloadCreator: (args: ActionParams, thunkArgs: TA) => Promise<Returned> | Returned): ((args: ActionParams) => (dispatch: TA["dispatch"], getState: TA["getState"], extra: TA["extra"]) => Promise<import("./createAction").PayloadAction<Returned, string, {
+    args: ActionParams;
+    requestId: string;
+}, never> | import("./createAction").PayloadAction<undefined, string, {
+    args: ActionParams;
+    requestId: string;
+}, Error>>) & {
     pending: import("./createAction").ActionCreatorWithPreparedPayload<[string, ActionParams], undefined, string, never, {
         args: ActionParams;
         requestId: string;
@@ -116,6 +121,13 @@ export function createAsyncThunk<ActionType extends string, Returned, ActionPara
         args: ActionParams;
         requestId: string;
     }>;
+    unwrapResult: (returned: import("./createAction").PayloadAction<Returned, string, {
+        args: ActionParams;
+        requestId: string;
+    }, never> | import("./createAction").PayloadAction<undefined, string, {
+        args: ActionParams;
+        requestId: string;
+    }, Error>) => Returned;
 };
 
 // @alpha (undocumented)
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
index 944b894f..1e0c2160 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -2,6 +2,8 @@ import { Dispatch } from 'redux'
 import nanoid from 'nanoid'
 import { createAction } from './createAction'
 
+type Await<P> = P extends PromiseLike<infer T> ? T : P
+
 type AsyncThunksArgs<S, E, D extends Dispatch = Dispatch> = {
   dispatch: D
   getState: S
@@ -101,34 +103,47 @@ export function createAsyncThunk<
     ) => {
       const requestId = nanoid()
 
-      let result: Returned
+      let finalAction: ReturnType<typeof fulfilled | typeof rejected>
       try {
         dispatch(pending(requestId, args))
 
-        result = (await payloadCreator(args, {
-          dispatch,
-          getState,
-          extra,
-          requestId
-        } as TA)) as Returned
+        finalAction = fulfilled(
+          await payloadCreator(args, {
+            dispatch,
+            getState,
+            extra,
+            requestId
+          } as TA),
+          requestId,
+          args
+        )
       } catch (err) {
         const serializedError = miniSerializeError(err)
-        dispatch(rejected(serializedError, requestId, args))
-        // Rethrow this so the user can handle if desired
-        throw err
+        finalAction = rejected(serializedError, requestId, args)
       }
 
       // We dispatch "success" _after_ the catch, to avoid having any errors
       // here get swallowed by the try/catch block,
       // per https://twitter.com/dan_abramov/status/770914221638942720
       // and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
-      return dispatch(fulfilled(result!, requestId, args))
+      dispatch(finalAction)
+      return finalAction
     }
   }
 
-  actionCreator.pending = pending
-  actionCreator.rejected = rejected
-  actionCreator.fulfilled = fulfilled
+  function unwrapResult(
+    returned: Await<ReturnType<ReturnType<typeof actionCreator>>>
+  ) {
+    if (rejected.match(returned)) {
+      throw returned.error
+    }
+    return returned.payload
+  }
 
-  return actionCreator
+  return Object.assign(actionCreator, {
+    pending,
+    rejected,
+    fulfilled,
+    unwrapResult
+  })
 }


Test patch:
diff --git a/type-tests/files/createAsyncThunk.typetest.ts b/type-tests/files/createAsyncThunk.typetest.ts
index 8badad5d..04a0bf11 100644
--- a/type-tests/files/createAsyncThunk.typetest.ts
+++ b/type-tests/files/createAsyncThunk.typetest.ts
@@ -1,5 +1,6 @@
-import { createAsyncThunk, Dispatch, createReducer } from 'src'
+import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
 import { ThunkDispatch } from 'redux-thunk'
+import { promises } from 'fs'
 
 function expectType<T>(t: T) {
   return t
@@ -7,20 +8,49 @@ function expectType<T>(t: T) {
 function fn() {}
 
 // basic usage
-{
-  const dispatch = fn as ThunkDispatch<any, any, any>
+;(async function() {
+  const dispatch = fn as ThunkDispatch<{}, any, AnyAction>
 
   const async = createAsyncThunk('test', (id: number) =>
     Promise.resolve(id * 2)
   )
-  dispatch(async(3))
 
   const reducer = createReducer({}, builder =>
     builder
-      .addCase(async.pending, (_, action) => {})
+      .addCase(async.pending, (_, action) => {
+        expectType<ReturnType<typeof async['pending']>>(action)
+      })
       .addCase(async.fulfilled, (_, action) => {
+        expectType<ReturnType<typeof async['fulfilled']>>(action)
         expectType<number>(action.payload)
       })
-      .addCase(async.rejected, (_, action) => {})
+      .addCase(async.rejected, (_, action) => {
+        expectType<ReturnType<typeof async['rejected']>>(action)
+        expectType<Error>(action.error)
+      })
   )
-}
+
+  const promise = dispatch(async(3))
+  const result = await promise
+
+  if (async.fulfilled.match(result)) {
+    expectType<ReturnType<typeof async['fulfilled']>>(result)
+    // typings:expect-error
+    expectType<ReturnType<typeof async['rejected']>>(result)
+  } else {
+    expectType<ReturnType<typeof async['rejected']>>(result)
+    // typings:expect-error
+    expectType<ReturnType<typeof async['fulfilled']>>(result)
+  }
+
+  promise
+    .then(async.unwrapResult)
+    .then(result => {
+      expectType<number>(result)
+      // typings:expect-error
+      expectType<Error>(result)
+    })
+    .catch(error => {
+      // catch is always any-typed, nothing we can do here
+    })
+})()