Repository: reduxjs/redux-toolkit
Pull request #393 (2020-02-25 22:47:04)

RFC createAsyncThunk: reject with typed value
Starting from #390, @msutkowski and I have been discussing this further over discord.

This would be one possible way to reject from a `payloadCreator`, but pass typed data (which would be impossible using `throw`, as that cannot be typed):
Somewhere within `payloadCreator`, an object passed through `rejectWithValue` is returned. That object then ends up as `payload` of a `rejected` action that is finally dispatched & returned by the async thunk.

See

https://github.com/phryneas/redux-toolkit/blob/44a550193646d309931405a107d66ea350f70f43/type-tests/files/createAsyncThunk.typetest.ts#L104-L123

This PR is essentially a starting point so @msutkowski can experiment with it in his example app.

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

Built with commit 44a550193646d309931405a107d66ea350f70f43

https://deploy-preview-393--redux-starter-kit-docs.netlify.com
markerikson: Cool, glad you two have been busy while I've been at work :)
phryneas: Apparently api-extractor doesn't like that I added a method on createAsyncThunk. To be honest, I'm not even sure if I like that method on there, so I'll leave it complaining for the moment. 
Feedback on that is very welcome.
phryneas: Okay, moved it on the `thunkApi` parameter. I think that fits a lot better because it doesn't need an extra import and also comes pre-typed correctly.
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/393/builds/12276) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 70ada977ba9a1e06afe1ded00666223c586fc33e:

|Sandbox| Source |
|--|--|
|[kind-shtern-dd26m](https://codesandbox.io/s/kind-shtern-dd26m)| Configuration |
|[empty-sun-3xm6u](https://codesandbox.io/s/empty-sun-3xm6u)| Configuration |
|[clever-kowalevski-hgndy](https://codesandbox.io/s/clever-kowalevski-hgndy)| Configuration |

markerikson: Yep, makes sense to me.  (And this is why I pushed to make it an options object instead of positional args.  I didn't know we'd add this specific thing, but as soon as we started looking at more than just `(dispatch, getState)`, it made sense to bundle them.)
msutkowski: Sorry about the delay in providing feedback here. 

### General feedback
These changes are great and very straightforward to work with. I don't see any problems with how the implementation is done, but I'm running into type issues. I'll do my best to outline the things I've found.

#### What I'm trying to achieve with `createAsyncThunk`
1. Handle the success and set the payload
2. On a known/expected error type I want to return it via `rejectWithValue`
3. On an unknown error, I'd like to throw it and just let `miniSerializeError` wrap it up


#### Issue when trying try/catching
When defining `get<CallsResponse>`, the type signature on `return data` is `Call[]`, matching the return type specificed `Call[]`. 
```ts
export const fetchLiveCallsError = createAsyncThunk<
  Call[],
  string,
  { state: RootState; rejectValue: RejectedErrorPayload<ValidationErrorsResponse> }
>("calls/fetchLiveCalls", async (organizationId, { rejectWithValue }) => {
  try {
    const {
      data: { data }
    } = await apiRequest.get<CallsResponse>(`organizations/${organizationId}/calls/live/iwill404`);
    return data;
  } catch (err) {
    let error: AxiosError = err; // cast for access to AxiosError properties
    return rejectWithValue({
      error: "just a test message",
      data: error.response?.data,
    });
  }
});
```
It throws this error (slightly cleaned up):
```  Type 'Promise<Call[] | RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>>' is not assignable to type 'Call[] | Promise<Call[]> | RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>> | Promise<...>'.
    Type 'Promise<Call[] | RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>>' is not assignable to type 'Promise<Call[]>'.
      Type 'Call[] | RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>' is not assignable to type 'Call[]'.
        Type 'RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>' is missing the following properties from type 'Call[]': length, pop, push, concat, and 28 more.
```

If I pull the Axios type of `CallsResponse` out, data is set to `any` and there are no errors but that doesn't seem like the right answer. 

#### Is it possible to add a type guard or something along the lines of this (`isRejectedValue`):

```ts
builder.addCase(fetchLiveCallsError.rejected, (state, action) => {
      // is something like this a good idea to export from the lib that would tell TS the rejected payload is correct and drop the | undefined-related errors
      const isRejectedValue = (check: typeof action) => check.error === 'Rejected'; 
     
      if (isRejectedValue(action)) {
        // this was rejected via rejectWithValue, handle it
        state.error = action.payload?.error;
      } else {
        /** 
         * was actually thrown and would have a mini serializable error format
         * action.error.name, action.error.stack, action.error.code, action.error.message should be available
         * but appear to be getting dropped
         * */
      }
    })
```

#### A full example with all handling methods
Per point 3, I imagine a 'complete' handling scenario would look like the below. Does this seem right? If so, how can we get the point after for component usage to be aware of the difference between the success result, rejected with value result, and the general rejected data?

```ts
export const fetchLiveCallsError = createAsyncThunk<
  Call[],
  string,
  { state: RootState; rejectValue: RejectedErrorPayload<ValidationErrorsResponse> }
>("calls/fetchLiveCalls", async (organizationId, { rejectWithValue }) => {
  try {
    const {
      data: { data }
    } = await apiRequest.get<CallsResponse>(`organizations/${organizationId}/calls/live/iwill404`);
    return data;
  } catch (err) {
    let error: AxiosError = err;
    // This is an expected error with validation errors - format + reject it
    if (error.response?.data?.details) {
      return rejectWithValue({
        error: "just a test message",
        data: error.response?.data,
      });
    }
    // just throw the unexpected error?
    throw err;
  }
});
```


#### Leveraging the resulting rejected/fulfilled values in a component
A common request is to be able to access the action like this:

```ts
<Button onClick={async() => {
              const resultAction = await dispatch(fetchLiveCallsError(organizationId)); // type is PayloadAction and resultAction.error never exists
              if  (resultAction.error === 'Rejected') {
                //  set form errors from the info that was passed into the rejected payload, etc etc.
              }
            }}>Perform call</Button>```

In this instance, someone could check for truthiness and cast it _(or if we can do the type guard idea from above, great)_, but is the ideal usage? Either way, it would appear that type union returned isn't accurate.
phryneas: 
> #### Issue when trying try/catching

...

> It throws this error (slightly cleaned up):
> 
> ```
>     Type 'Promise<Call[] | RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>>' is not assignable to type 'Promise<Call[]>'.
>       Type 'Call[] | RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>' is not assignable to type 'Call[]'.
>         Type 'RejectWithValue<RejectedErrorPayload<ValidationErrorsResponse>>' is missing the following properties from type 'Call[]': length, pop, push, concat, and 28 more.
> ```

This should be fixed, thanks for noticing!

> If I pull the Axios type of `CallsResponse` out, data is set to `any` and there are no errors but that doesn't seem like the right answer.
> #### Is it possible to add a type guard or something along the lines of this (`isRejectedValue`):

You are setting
```ts
let error: AxiosError = err // cast for access to AxiosError properties
```
which defaults to `AxiosError<any>`, so in the rejected value, data will result in `any`.

You can use `AxiosError<MyApiErrorType>` instead.

> 
> ```ts
> builder.addCase(fetchLiveCallsError.rejected, (state, action) => {
>       // is something like this a good idea to export from the lib that would tell TS the rejected payload is correct and drop the | undefined-related errors
>       const isRejectedValue = (check: typeof action) => check.error === 'Rejected'; 
>      
>       if (isRejectedValue(action)) {
>         // this was rejected via rejectWithValue, handle it
>         state.error = action.payload?.error;
>       } else {
>         /** 
>          * was actually thrown and would have a mini serializable error format
>          * action.error.name, action.error.stack, action.error.code, action.error.message should be available
>          * but appear to be getting dropped
>          * */
>       }
>     })
> ```

I'd do this the other way around: instead of `if (isRejectedValue(action))`, just to `if (action.payload)`, because the existence of a payload is exactly what separates a rejection with value from one without.


For the "mini serializable error format": yes, but now. Currently, `miniSerializeError` returns `any`, because if you put a non-object like a string into it, it will return exactly that and not a `SerializedError`. This might be unwanted behaviour though. @markerikson what do you think?

> 
> #### A full example with all handling methods
> 
> Per point 3, I imagine a 'complete' handling scenario would look like the below. Does this seem right? If so, how can we get the point after for component usage to be aware of the difference between the success result, rejected with value result, and the general rejected data?
> 
> ```ts
> export const fetchLiveCallsError = createAsyncThunk<
>   Call[],
>   string,
>   { state: RootState; rejectValue: RejectedErrorPayload<ValidationErrorsResponse> }
> >("calls/fetchLiveCalls", async (organizationId, { rejectWithValue }) => {
>   try {
>     const {
>       data: { data }
>     } = await apiRequest.get<CallsResponse>(`organizations/${organizationId}/calls/live/iwill404`);
>     return data;
>   } catch (err) {
>     let error: AxiosError = err;
>     // This is an expected error with validation errors - format + reject it
>     if (error.response?.data?.details) {
>       return rejectWithValue({
>         error: "just a test message",
>         data: error.response?.data,
>       });
>     }
>     // just throw the unexpected error?
>     throw err;
>   }
> });
> ```
> 
> #### Leveraging the resulting rejected/fulfilled values in a component
> 
> A common request is to be able to access the action like this:
> 
> ```ts
> <Button onClick={async() => {
>               const resultAction = await dispatch(fetchLiveCallsError(organizationId)); // type is PayloadAction and resultAction.error never exists
>               if  (resultAction.error === 'Rejected') {
>                 //  set form errors from the info that was passed into the rejected payload, etc etc.
>               }
>             }}>Perform call</Button>```
> 
> In this instance, someone could check for truthiness and cast it _(or if we can do the type guard idea from above, great)_, but is the ideal usage? Either way, it would appear that type union returned isn't accurate.
> ```

I'd probably do something along the lines of

```ts
 if (fetchLiveCallsError.fulfilled.match(result)) {
      //success
      expectType<ReturnType<typeof fetchLiveCallsError['fulfilled']>>(result)
      expectType<Call[]>(result.payload)
    } else {
      expectType<ReturnType<typeof fetchLiveCallsError['rejected']>>(result)
      if (result.payload) {
        // rejected with value
        expectType<RejectedErrorPayload<ValidationErrorsResponse>>(
          result.payload
        )
      } else {
        // rejected by throw
        expectType<undefined>(result.payload)
        // result.error is `any`, because `miniSerializeError` currently returns any (if you pass in a non-object, it returns it and not a SerializedError)
        expectType<IsAny<typeof result['error'], true, false>>(true)
      }
    }
```
msutkowski: Thanks, this all works as expected now. The component usage is also straightforward as you explained and usage is really simple.
markerikson: Sorry, what's the question / suggestion around serialized errors?
phryneas: > Sorry, what's the question / suggestion around serialized errors?

Sorry, my bad.

Currently, `reject` returns the result of `miniSerializeError` on it's `error` property. That is not a `SerializedError` though, but `any`, as `miniSerializeError` in the current form does not always return a `SerializedError`. It does so, if the passed argument is an `object`, but otherwise it just returns it's input.
So my question is: is this intentional and do we want to leave it that way? Or do we want to make a change alike this, to make the shape of `error` more predictable

```diff
-export const miniSerializeError = (value: any): any => {
+export const miniSerializeError = (value: any): SerializedError => {
  if (typeof value === 'object' && value !== null) {
    // ...
    return simpleError
  }

-  return value
+  return { message: String(value) }
}
```
markerikson: If you feel that would be more consistent, sure, we can go with that.
phryneas: So I'm not really sure, are the CI fails just from immer not working with 3.5/3.6 right now?
Other than that I *think* this should be good go go.
Note: I did add `axios` as a `devDependency` because I used their types in a type-test. Could do another test, but it gives it kind of a nice "real-life" spin. Could we keep that?
markerikson: Yeah, I expect TS 3.5 and 3.6 to fail right now due to the Immer alpha.

Can you update the docs as well?
phryneas: I just rewrote the history of this, so we can merge this without squashing to keep @msutkowski's contributions under his name.
phryneas: And we have a mocked AbortController. It looks good from the tests, but I have no IE at hand, so I can only test this in theory. Anyways, I guess we'll get that feedback from some alpha testers ^^. 
phryneas: The AbortController seems to cost us quite a bit though.

Stats before last commit:



| Filename | Size | Change | |
|:--- |:---:|:---:|:---:|
| `dist/redux-toolkit.cjs.development.js` | 9.69 kB | +92 B (0%) |  |
| `dist/redux-toolkit.cjs.production.min.js` | 3.99 kB | +69 B (1%) |  |
| `dist/redux-toolkit.esm.js` | 9.6 kB | +99 B (1%) |  |
| `dist/redux-toolkit.umd.js` | 20.9 kB | +94 B (0%) |  |
| `dist/redux-toolkit.umd.min.js` | 9.49 kB | +65 B (0%) |  |

Stats after:


| Filename | Size | Change | |
|:--- |:---:|:---:|:---:|
| `dist/redux-toolkit.cjs.development.js` | 9.97 kB | +367 B (3%) |  |
| `dist/redux-toolkit.cjs.production.min.js` | 4.18 kB | +256 B (6%) | 🔍 |
| `dist/redux-toolkit.esm.js` | 9.88 kB | +381 B (3%) |  |
| `dist/redux-toolkit.umd.js` | 21.1 kB | +369 B (1%) |  |
| `dist/redux-toolkit.umd.min.js` | 9.67 kB | +246 B (2%) |  |
markerikson: Hmm. Yeah. I wonder if it's partly because of the class being transpiled. Any way to mimic that as a plain object / function that might end up being smaller?
phryneas: I think I just forgot to remove the comment around my NODE_ENV check... checking it right now.
phryneas: Yeah, now i'ts looking better:



| Filename | Size | Change | |
|:--- |:---:|:---:|:---:|
| `dist/redux-toolkit.cjs.development.js` | 9.95 kB | +351 B (3%) |  |
| `dist/redux-toolkit.cjs.production.min.js` | 4.05 kB | +126 B (3%) |  |
| `dist/redux-toolkit.esm.js` | 9.87 kB | +371 B (3%) |  |
| `dist/redux-toolkit.umd.js` | 21.1 kB | +351 B (1%) |  |
| `dist/redux-toolkit.umd.min.js` | 9.55 kB | +127 B (1%) |  |



Oh, by the way: the error message from the nonSerializableMiddleware seems to take up ~0.5k - maybe we should shorten that in the prod build?
markerikson: Both the immutable and serializable middleware are stripped out in prod entirely, so they'll never show up in the actual prod bundle.
phryneas: Nope, this is from `redux-toolkit.cjs.production.min.js`:

```
a&&console.error("A non-serializable value was detected in an action, in the path: `"+a.keyPath+"`. Value:",a.value,"\nTake a look at the logic that dispatched this action: ",i,"\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)");var c=t(i),s=l(e.getState(),[],r,n,u);return s&&console.error("A non-serializable value was detected in the state, in the path: `"+s.keyPath+"`. Value:",s.value,"\nTake a look at the reducer(s) handling this action type: "+i.type+".\n(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
```
markerikson: Oh dear.  That shouldn't be in there.

Tell you what.  Let's get this PR merged in now.  I'm at work and theoretically supposed to be doing something productive atm.  Can you take a look at why that error is even ending up in the prod build?
phryneas: Can do :)

Patch:
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
index 18d568c4..3110795c 100644
--- a/docs/api/createAsyncThunk.md
+++ b/docs/api/createAsyncThunk.md
@@ -64,7 +64,7 @@ For example, a `type` argument of `'users/requestStatus'` will generate these ac
 
 ### `payloadCreator`
 
-A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should return a rejected promise containing either an `Error` instance or a plain value such as a descriptive error message.
+A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an `Error` instance or a plain value such as a descriptive error message or otherwise a resolved promise with a `RejectWithValue` argument as returned by the `thunkApi.rejectWithValue` function.
 
 The `payloadCreator` function can contain whatever logic you need to calculate an appropriate result. This could include a standard AJAX data fetch request, multiple AJAX calls with the results combined into a final value, interactions with React Native `AsyncStorage`, and so on.
 
@@ -77,6 +77,7 @@ The `payloadCreator` function will be called with two arguments:
   - `extra`: the "extra argument" given to the thunk middleware on setup, if available
   - `requestId`: a unique string ID value that was automatically generated to identify this request sequence
   - `signal`: an [`AbortController.signal` object](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal) that may be used to see if another part of the app logic has marked this request as needing cancelation.
+  - `rejectWithValue`: rejectWithValue is a utility function that you can `return` in your action creator to return a rejected response with a defined payload. It will pass whatever value you give it and return it in the payload of the rejected action.
 
 The logic in the `payloadCreator` function may use any of these values as needed to calculate the result.
 
@@ -90,7 +91,8 @@ When dispatched, the thunk will:
 - call the `payloadCreator` callback and wait for the returned promise to settle
 - when the promise settles:
   - if the promise resolved successfully, dispatch the `fulfilled` action with the promise value as `action.payload`
-  - if the promise failed, dispatch the `rejected` action with a serialized version of the error value as `action.error`
+  - if the promise resolved with a `rejectWithValue(value)` return value, dispatch the `rejected` action with the value passed into `action.payload` and 'Rejected' as `action.error.message`
+  - if the promise failed and was not handled with `rejectWithValue`, dispatch the `rejected` action with a serialized version of the error value as `action.error`
 - Return a fulfilled promise containing the final dispatched action (either the `fulfilled` or `rejected` action object)
 
 ## Promise Lifecycle Actions
@@ -99,7 +101,7 @@ When dispatched, the thunk will:
 
 The action creators will have these signatures:
 
-```ts
+```typescript
 interface SerializedError {
   name?: string
   message?: string
@@ -136,6 +138,17 @@ interface RejectedAction<ThunkArg> {
   }
 }
 
+interface RejectedWithValueAction<ThunkArg, RejectedValue> {
+  type: string
+  payload: RejectedValue
+  error: { message: 'Rejected' }
+  meta: {
+    requestId: string
+    arg: ThunkArg
+    aborted: boolean
+  }
+}
+
 type Pending = <ThunkArg>(
   requestId: string,
   arg: ThunkArg
@@ -151,6 +164,11 @@ type Rejected = <ThunkArg>(
   requestId: string,
   arg: ThunkArg
 ) => RejectedAction<ThunkArg>
+
+type RejectedWithValue = <ThunkArg, RejectedValue>(
+  requestId: string,
+  arg: ThunkArg
+) => RejectedWithValueAction<ThunkArg, RejectedValue>
 ```
 
 To handle these actions in your reducers, reference the action creators in `createReducer` or `createSlice` using either the object key notation or the "builder callback" notation:
@@ -299,7 +317,7 @@ const fetchUserById = createAsyncThunk(
 
 ## Examples
 
-Requesting a user by ID, with loading state, and only one request at a time:
+- Requesting a user by ID, with loading state, and only one request at a time:
 
 ```js
 import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
@@ -352,7 +370,7 @@ const UsersComponent = () => {
 
   const fetchOneUser = async userId => {
     try {
-      const resultAction = dispatch(fetchUserById(userId))
+      const resultAction = await dispatch(fetchUserById(userId))
       const user = unwrapResult(resultAction)
       showToast('success', `Fetched ${user.name}`)
     } catch (err) {
@@ -363,3 +381,181 @@ const UsersComponent = () => {
   // render UI here
 }
 ```
+
+- Using rejectWithValue to access a custom rejected payload in a component
+
+```js
+import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+
+const updateUser = createAsyncThunk(
+  'users/update',
+  async (userData, { rejectWithValue }) => {
+    const { id, ...fields } = userData
+    try {
+      const response = await userAPI.updateById(id, fields)
+      return response.data.user
+    } catch (err) {
+      // Note: this is an example assuming the usage of axios. Other fetching libraries would likely have different implementations
+      if (!err.response) {
+        throw err
+      }
+
+      return rejectWithValue(err.response.data)
+    }
+  }
+)
+
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: {
+    entities: {},
+    error: null
+  },
+  reducers: {},
+  extraReducers: {
+    [updateUser.fullfilled]: (state, action) => {
+      const user = action.payload
+      state.entities[user.id] = user
+    },
+    [updateUser.rejected]: (state, action) => {
+      if (action.payload) {
+        // If a rejected action has a payload, it means that it was returned with rejectWithValue
+        state.error = action.payload.errorMessage
+      } else {
+        state.error = action.error
+      }
+    }
+  }
+})
+
+const UsersComponent = () => {
+  const { users, loading, error } = useSelector(state => state.users)
+  const dispatch = useDispatch()
+
+  // This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action
+  const handleUpdateUser = async (values, formikHelpers) => {
+    const resultAction = await dispatch(updateUser(values))
+    if (updateUser.fulfilled.match(resultAction)) {
+      const user = unwrapResult(resultAction)
+      showToast('success', `Updated ${user.name}`)
+    } else {
+      if (resultAction.payload) {
+        // This is assuming the api returned a 400 error with a body of { errorMessage: 'Validation errors', field_errors: { field_name: 'Should be a string' } }
+        formikHelpers.setErrors(resultAction.payload.field_errors)
+      } else {
+        showToast('error', `Update failed: ${resultAction.error}`)
+      }
+    }
+  }
+
+  // render UI here
+}
+```
+
+- TypeScript: Using rejectWithValue to access a custom rejected payload in a component
+  _Note: this is a contrived example assuming our userAPI only ever throws validation-specific errors_
+
+```typescript
+import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+import { AppDispatch, RootState } from '../store'
+import { FormikHelpers } from 'formik'
+
+// Sample types that will be used
+interface User {
+  first_name: string
+  last_name: string
+  email: string
+}
+
+interface ValidationErrors {
+  errorMessage: string
+  field_errors: Record<string, string>
+}
+
+interface UpdateUserResponse {
+  user: User
+  success: boolean
+}
+
+const updateUser = createAsyncThunk<
+  User,
+  Partial<User>,
+  {
+    rejectValue: ValidationErrors
+  }
+>('users/update', async (userData, { rejectWithValue }) => {
+  try {
+    const { id, ...fields } = userData
+    const response = await userAPI.updateById<UpdateUserResponse>(id, fields)
+    return response.data.user
+  } catch (err) {
+    let error: AxiosError<ValidationErrors> = err // cast the error for access
+    if (!error.response) {
+      throw err
+    }
+    // We got validation errors, let's return those so we can reference in our component and set form errors
+    return rejectWithValue(error.response.data)
+  }
+})
+
+interface UsersState {
+  error: string | null
+  entities: Record<string, User>
+}
+
+const initialState: UsersState = {
+  entities: {},
+  error: null
+}
+
+const usersSlice = createSlice({
+  name: 'users',
+  initialState,
+  reducers: {},
+  extraReducers: builder => {
+    // The `builder` callback form is used here because it provides correctly typed reducers from the action creators
+    builder.addCase(updateUser.fulfilled, (state, { payload }) => {
+      state.entities[payload.id] = payload
+    })
+    builder.addCase(updateUser.rejected, (state, action) => {
+      if (action.payload) {
+        // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here.
+        state.error = action.payload.errorMessage
+      } else {
+        state.error = action.error
+      }
+    })
+  }
+})
+
+const UsersComponent = () => {
+  const { users, loading, error } = useSelector(
+    (state: RootState) => state.users
+  )
+  const dispatch: AppDispatch = useDispatch()
+
+  // This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action
+  const handleUpdateUser = async (
+    values: FormValues,
+    formikHelpers: FormikHelpers<FormValues>
+  ) => {
+    const resultAction = await dispatch(updateUser(values))
+    if (updateUser.fulfilled.match(resultAction)) {
+      // user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk
+      const user = unwrapResult(resultAction)
+      showToast('success', `Updated ${user.name}`)
+    } else {
+      if (resultAction.payload) {
+        // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here.
+        formikHelpers.setErrors(resultAction.payload.field_errors)
+      } else {
+        showToast('error', `Update failed: ${resultAction.error}`)
+      }
+    }
+  }
+
+  // render UI here
+}
+```
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index f084fa2d..0ae3a309 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -420,14 +420,14 @@ const fetchUserById = createAsyncThunk(
 const lastReturnedAction = await store.dispatch(fetchUserById(3))
 ```
 
-The second argument to the `payloadCreator`, known as `thunkApi`, is an object containing references to the `dispatch`, `getState`, and `extra` arguments from the thunk middleware. If you want to use these from within the `payloadCreator`, you will need to define some generic arguments, as the types for these arguments cannot be inferred. Also, as TS cannot mix explicit and inferred generic parameters, from this point on you'll have to define the `Returned` and `ThunkArg` generic parameter as well.
+The second argument to the `payloadCreator`, known as `thunkApi`, is an object containing references to the `dispatch`, `getState`, and `extra` arguments from the thunk middleware as well as a utility function called `rejectWithValue`. If you want to use these from within the `payloadCreator`, you will need to define some generic arguments, as the types for these arguments cannot be inferred. Also, as TS cannot mix explicit and inferred generic parameters, from this point on you'll have to define the `Returned` and `ThunkArg` generic parameter as well.
 
-To define the types for these arguments, pass an object as the third generic argument, with type declarations for some or all of these fields: `{dispatch?, state?, extra?}`.
+To define the types for these arguments, pass an object as the third generic argument, with type declarations for some or all of these fields: `{dispatch?, state?, extra?, rejectValue?}`.
 
-```ts {2-12}
+```ts
 const fetchUserById = createAsyncThunk<
   // Return type of the payload creator
-  Promise<MyData>,
+  MyData,
   // First argument to the payload creator
   number,
   {
@@ -447,7 +447,98 @@ const fetchUserById = createAsyncThunk<
 })
 ```
 
-While this notation for `state`, `dispatch` and `extra` might seem uncommon at first, it allows you to provide only the types for these you actually need - so for example, if you are not accessing `getState` within your `payloadCreator`, there is no need to provide a type for `state`.
+If you are performing a request that you know will typically either be a success or have an expected error format, you can pass in a type to `rejectValue` and `return rejectWithValue(knownPayload)` in the action creator. This allows you to reference the error payload in the reducer as well as in a component after dispatching the `createAsyncThunk` action.
+
+```ts
+interface MyKnownError {
+  errorMessage: string
+  // ...
+}
+interface UserAttributes {
+  id: string
+  first_name: string
+  last_name: string
+  email: string
+}
+
+const updateUser = createAsyncThunk<
+  // Return type of the payload creator
+  MyData,
+  // First argument to the payload creator
+  UserAttributes,
+  // Types for ThunkAPI
+  {
+    extra: {
+      jwt: string
+    }
+    rejectValue: MyKnownError
+  }
+>('users/update', async (user, thunkApi) => {
+  const { id, ...userData } = user
+  const response = await fetch(`https://reqres.in/api/users/${id}`, {
+    method: 'PUT',
+    headers: {
+      Authorization: `Bearer ${thunkApi.extra.jwt}`
+    },
+    body: JSON.stringify(userData)
+  })
+  if (response.status === 400) {
+    // Return the known error for future handling
+    return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
+  }
+  return (await response.json()) as MyData
+})
+```
+
+While this notation for `state`, `dispatch`, `extra` and `rejectValue` might seem uncommon at first, it allows you to provide only the types for these you actually need - so for example, if you are not accessing `getState` within your `payloadCreator`, there is no need to provide a type for `state`. The same can be said about `rejectValue` - if you don't need to access any potential error payload, you can ignore it.
+
+In addition, you can leverage checks against `action.payload` and `match` as provided by `createAction` as a type-guard for when you want to access known properties on defined types. Example:
+
+- In a reducer
+
+```ts
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: {
+    entities: {},
+    error: null
+  },
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(updateUser.fulfilled, (state, { payload }) => {
+      state.entities[payload.id] = payload
+    })
+    builder.addCase(updateUser.rejected, (state, action) => {
+      if (action.payload) {
+        // Since we passed in `MyKnownError` to `rejectType` in `updateUser`, the type information will be available here.
+        state.error = action.payload.errorMessage
+      } else {
+        state.error = action.error
+      }
+    })
+  }
+})
+```
+
+- In a component
+
+```ts
+const handleUpdateUser = async userData => {
+  const resultAction = await dispatch(updateUser(userData))
+  if (updateUser.fulfilled.match(resultAction)) {
+    const user = unwrapResult(resultAction)
+    showToast('success', `Updated ${user.name}`)
+  } else {
+    if (resultAction.payload) {
+      // Since we passed in `MyKnownError` to `rejectType` in `updateUser`, the type information will be available here.
+      // Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
+      showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
+    } else {
+      showToast('error', `Update failed: ${resultAction.error.message}`)
+    }
+  }
+}
+```
 
 ## `createEntityAdapter`
 
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index a4ac2c2d..8b19b879 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -103,21 +103,21 @@ 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<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned> | Returned): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
+export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>> | Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
     arg: ThunkArg;
     requestId: string;
-}, never> | PayloadAction<undefined, string, {
+}, never> | PayloadAction<GetRejectValue<ThunkApiConfig> | undefined, string, {
     arg: ThunkArg;
     requestId: string;
     aborted: boolean;
-}, any>> & {
+}, SerializedError>> & {
     abort: (reason?: string | undefined) => void;
 }) & {
     pending: ActionCreatorWithPreparedPayload<[string, ThunkArg], undefined, string, never, {
         arg: ThunkArg;
         requestId: string;
     }>;
-    rejected: ActionCreatorWithPreparedPayload<[Error, string, ThunkArg], undefined, string, any, {
+    rejected: ActionCreatorWithPreparedPayload<[Error | null, string, ThunkArg, (GetRejectValue<ThunkApiConfig> | undefined)?], GetRejectValue<ThunkApiConfig> | undefined, string, SerializedError, {
         arg: ThunkArg;
         requestId: string;
         aborted: boolean;
@@ -284,11 +284,7 @@ export type SliceCaseReducers<State> = {
 export { ThunkAction }
 
 // @alpha (undocumented)
-export function unwrapResult<T>(returned: {
-    error: any;
-} | {
-    payload: NonNullable<T>;
-}): NonNullable<T>;
+export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(returned: R): PayloadForActionTypesExcludingErrorActions<R>;
 
 // @alpha (undocumented)
 export type Update<T> = {
diff --git a/package-lock.json b/package-lock.json
index aefde313..5b27f99e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1688,6 +1688,15 @@
       "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==",
       "dev": true
     },
+    "axios": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+      "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+      "dev": true,
+      "requires": {
+        "follow-redirects": "1.5.10"
+      }
+    },
     "axobject-query": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.1.tgz",
@@ -3618,6 +3627,32 @@
       "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
       "dev": true
     },
+    "follow-redirects": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+      "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+      "dev": true,
+      "requires": {
+        "debug": "=3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -3689,8 +3724,7 @@
           "version": "2.1.1",
           "resolved": false,
           "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -3714,15 +3748,13 @@
           "version": "1.0.0",
           "resolved": false,
           "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "resolved": false,
           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -3739,22 +3771,19 @@
           "version": "1.1.0",
           "resolved": false,
           "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "resolved": false,
           "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "resolved": false,
           "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -3885,8 +3914,7 @@
           "version": "2.0.3",
           "resolved": false,
           "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -3900,7 +3928,6 @@
           "resolved": false,
           "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -3917,7 +3944,6 @@
           "resolved": false,
           "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -3926,15 +3952,13 @@
           "version": "0.0.8",
           "resolved": false,
           "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.3.5",
           "resolved": false,
           "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -3955,7 +3979,6 @@
           "resolved": false,
           "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -4044,8 +4067,7 @@
           "version": "1.0.1",
           "resolved": false,
           "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -4059,7 +4081,6 @@
           "resolved": false,
           "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -4155,8 +4176,7 @@
           "version": "5.1.2",
           "resolved": false,
           "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -4198,7 +4218,6 @@
           "resolved": false,
           "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -4220,7 +4239,6 @@
           "resolved": false,
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
-          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -4269,15 +4287,13 @@
           "version": "1.0.2",
           "resolved": false,
           "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "yallist": {
           "version": "3.0.3",
           "resolved": false,
           "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
-          "dev": true,
-          "optional": true
+          "dev": true
         }
       }
     },
diff --git a/package.json b/package.json
index 5b6a370f..92a39e9b 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
     "@types/json-stringify-safe": "^5.0.0",
     "@types/nanoid": "^2.1.0",
     "@types/node": "^10.14.4",
+    "axios": "^0.19.2",
     "console-testing-library": "^0.3.1",
     "eslint-config-react-app": "^5.0.1",
     "invariant": "^2.2.4",
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
index 5c2ed4ea..4d2d7071 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -11,12 +11,18 @@ import { nanoid } from './nanoid'
 // @ts-ignore we need the import of these types due to a bundling issue.
 type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
 
-export type BaseThunkAPI<S, E, D extends Dispatch = Dispatch> = {
+export type BaseThunkAPI<
+  S,
+  E,
+  D extends Dispatch = Dispatch,
+  RejectedValue = undefined
+> = {
   dispatch: D
   getState: () => S
   extra: E
   requestId: string
   signal: AbortSignal
+  rejectWithValue(value: RejectedValue): RejectWithValue<RejectedValue>
 }
 
 /**
@@ -29,15 +35,19 @@ export interface SerializedError {
   code?: string
 }
 
-const commonProperties: (keyof SerializedError)[] = [
+const commonProperties: Array<keyof SerializedError> = [
   'name',
   'message',
   'stack',
   'code'
 ]
 
+class RejectWithValue<RejectValue> {
+  constructor(public readonly value: RejectValue) {}
+}
+
 // Reworked from https://github.com/sindresorhus/serialize-error
-export const miniSerializeError = (value: any): any => {
+export const miniSerializeError = (value: any): SerializedError => {
   if (typeof value === 'object' && value !== null) {
     const simpleError: SerializedError = {}
     for (const property of commonProperties) {
@@ -49,13 +59,14 @@ export const miniSerializeError = (value: any): any => {
     return simpleError
   }
 
-  return value
+  return { message: String(value) }
 }
 
 type AsyncThunkConfig = {
   state?: unknown
   dispatch?: Dispatch
   extra?: unknown
+  rejectValue?: unknown
 }
 
 type GetState<ThunkApiConfig> = ThunkApiConfig extends {
@@ -82,9 +93,16 @@ type GetDispatch<ThunkApiConfig> = ThunkApiConfig extends {
 type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
   GetState<ThunkApiConfig>,
   GetExtra<ThunkApiConfig>,
-  GetDispatch<ThunkApiConfig>
+  GetDispatch<ThunkApiConfig>,
+  GetRejectValue<ThunkApiConfig>
 >
 
+type GetRejectValue<ThunkApiConfig> = ThunkApiConfig extends {
+  rejectValue: infer RejectValue
+}
+  ? RejectValue
+  : unknown
+
 /**
  *
  * @param type
@@ -101,8 +119,13 @@ export function createAsyncThunk<
   payloadCreator: (
     arg: ThunkArg,
     thunkAPI: GetThunkAPI<ThunkApiConfig>
-  ) => Promise<Returned> | Returned
+  ) =>
+    | Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>>
+    | Returned
+    | RejectWithValue<GetRejectValue<ThunkApiConfig>>
 ) {
+  type RejectedValue = GetRejectValue<ThunkApiConfig>
+
   const fulfilled = createAction(
     type + '/fulfilled',
     (result: Returned, requestId: string, arg: ThunkArg) => {
@@ -125,11 +148,16 @@ export function createAsyncThunk<
 
   const rejected = createAction(
     type + '/rejected',
-    (error: Error, requestId: string, arg: ThunkArg) => {
-      const aborted = error && error.name === 'AbortError'
+    (
+      error: Error | null,
+      requestId: string,
+      arg: ThunkArg,
+      payload?: RejectedValue
+    ) => {
+      const aborted = !!error && error.name === 'AbortError'
       return {
-        payload: undefined,
-        error: miniSerializeError(error),
+        payload,
+        error: miniSerializeError(error || 'Rejected'),
         meta: {
           arg,
           requestId,
@@ -139,6 +167,34 @@ export function createAsyncThunk<
     }
   )
 
+  let displayedWarning = false
+
+  const AC =
+    typeof AbortController !== 'undefined'
+      ? AbortController
+      : class implements AbortController {
+          signal: AbortSignal = {
+            aborted: false,
+            addEventListener() {},
+            dispatchEvent() {
+              return false
+            },
+            onabort() {},
+            removeEventListener() {}
+          }
+          abort() {
+            if (process.env.NODE_ENV === 'development') {
+              if (!displayedWarning) {
+                displayedWarning = true
+                console.info(
+                  `This platform does not implement AbortController. 
+If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'.`
+                )
+              }
+            }
+          }
+        }
+
   function actionCreator(arg: ThunkArg) {
     return (
       dispatch: GetDispatch<ThunkApiConfig>,
@@ -147,7 +203,7 @@ export function createAsyncThunk<
     ) => {
       const requestId = nanoid()
 
-      const abortController = new AbortController()
+      const abortController = new AC()
       let abortReason: string | undefined
 
       const abortedPromise = new Promise<never>((_, reject) =>
@@ -173,9 +229,17 @@ export function createAsyncThunk<
                 getState,
                 extra,
                 requestId,
-                signal: abortController.signal
+                signal: abortController.signal,
+                rejectWithValue(value: RejectedValue) {
+                  return new RejectWithValue(value)
+                }
               })
-            ).then(result => fulfilled(result, requestId, arg))
+            ).then(result => {
+              if (result instanceof RejectWithValue) {
+                return rejected(null, requestId, arg, result.value)
+              }
+              return fulfilled(result, requestId, arg)
+            })
           ])
         } catch (err) {
           finalAction = rejected(err, requestId, arg)
@@ -199,14 +263,23 @@ export function createAsyncThunk<
   })
 }
 
+type ActionTypesWithOptionalErrorAction =
+  | { error: any }
+  | { error?: never; payload: any }
+type PayloadForActionTypesExcludingErrorActions<T> = T extends { error: any }
+  ? never
+  : T extends { payload: infer P }
+  ? P
+  : never
+
 /**
  * @alpha
  */
-export function unwrapResult<T>(
-  returned: { error: any } | { payload: NonNullable<T> }
-): NonNullable<T> {
+export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(
+  returned: R
+): PayloadForActionTypesExcludingErrorActions<R> {
   if ('error' in returned) {
     throw returned.error
   }
-  return returned.payload
+  return (returned as any).payload
 }


Test patch:
diff --git a/src/createAsyncThunk.test.ts b/src/createAsyncThunk.test.ts
index dd8ffbb6..c6669623 100644
--- a/src/createAsyncThunk.test.ts
+++ b/src/createAsyncThunk.test.ts
@@ -6,6 +6,12 @@ import {
 import { configureStore } from './configureStore'
 import { AnyAction } from 'redux'
 
+import {
+  mockConsole,
+  createConsole,
+  getLog
+} from 'console-testing-library/pure'
+
 describe('createAsyncThunk', () => {
   it('creates the action types', () => {
     const thunkActionCreator = createAsyncThunk('testType', async () => 42)
@@ -106,6 +112,181 @@ describe('createAsyncThunk', () => {
     expect(errorAction.meta.requestId).toBe(generatedRequestId)
     expect(errorAction.meta.arg).toBe(args)
   })
+
+  it('dispatches an empty error when throwing a random object without serializedError properties', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const errorObject = { wny: 'dothis' }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId }) => {
+        generatedRequestId = requestId
+        throw errorObject
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual({})
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches an action with a formatted error when throwing an object with known error keys', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const errorObject = {
+      name: 'Custom thrown error',
+      message: 'This is not necessary',
+      code: '400'
+    }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId }) => {
+        generatedRequestId = requestId
+        throw errorObject
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual(miniSerializeError(errorObject))
+    expect(Object.keys(errorAction.error)).not.toContain('stack')
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches a rejected action with a customized payload when a user returns rejectWithValue()', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const errorPayload = {
+      errorMessage:
+        'I am a fake server-provided 400 payload with validation details',
+      errors: [
+        { field_one: 'Must be a string' },
+        { field_two: 'Must be a number' }
+      ]
+    }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId, rejectWithValue }) => {
+        generatedRequestId = requestId
+
+        return rejectWithValue(errorPayload)
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+
+    expect(errorAction.error.message).toEqual('Rejected')
+    expect(errorAction.payload).toBe(errorPayload)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches a rejected action with a miniSerializeError when rejectWithValue conditions are not satisfied', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const error = new Error('Panic!')
+
+    const errorPayload = {
+      errorMessage:
+        'I am a fake server-provided 400 payload with validation details',
+      errors: [
+        { field_one: 'Must be a string' },
+        { field_two: 'Must be a number' }
+      ]
+    }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId, rejectWithValue }) => {
+        generatedRequestId = requestId
+
+        try {
+          throw error
+        } catch (err) {
+          if (!err.response) {
+            throw err
+          }
+          return rejectWithValue(errorPayload)
+        }
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual(miniSerializeError(error))
+    expect(errorAction.payload).toEqual(undefined)
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
 })
 
 describe('createAsyncThunk with abortController', () => {
@@ -233,4 +414,46 @@ describe('createAsyncThunk with abortController', () => {
       meta: { aborted: true }
     })
   })
+
+  describe('behaviour with missing AbortController', () => {
+    let keepAbortController: typeof AbortController
+    let freshlyLoadedModule: typeof import('./createAsyncThunk')
+    let restore: () => void
+    let nodeEnv: string
+
+    beforeEach(() => {
+      keepAbortController = window.AbortController
+      delete window.AbortController
+      jest.resetModules()
+      freshlyLoadedModule = require('./createAsyncThunk')
+      restore = mockConsole(createConsole())
+      nodeEnv = process.env.NODE_ENV!
+      process.env.NODE_ENV = 'development'
+    })
+
+    afterEach(() => {
+      process.env.NODE_ENV = nodeEnv
+      restore()
+      window.AbortController = keepAbortController
+      jest.resetModules()
+    })
+
+    test('calling `abort` on an asyncThunk works with a FallbackAbortController if no global abortController is not available', async () => {
+      const longRunningAsyncThunk = freshlyLoadedModule.createAsyncThunk(
+        'longRunning',
+        async () => {
+          await new Promise(resolve => setTimeout(resolve, 30000))
+        }
+      )
+
+      store.dispatch(longRunningAsyncThunk()).abort()
+      // should only log once, even if called twice
+      store.dispatch(longRunningAsyncThunk()).abort()
+
+      expect(getLog().log).toMatchInlineSnapshot(`
+        "This platform does not implement AbortController. 
+        If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'."
+      `)
+    })
+  })
 })
diff --git a/type-tests/files/createAsyncThunk.typetest.ts b/type-tests/files/createAsyncThunk.typetest.ts
index 58f0bc22..261f459e 100644
--- a/type-tests/files/createAsyncThunk.typetest.ts
+++ b/type-tests/files/createAsyncThunk.typetest.ts
@@ -1,7 +1,9 @@
 import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
 import { ThunkDispatch } from 'redux-thunk'
-import { promises } from 'fs'
-import { unwrapResult } from 'src/createAsyncThunk'
+import { unwrapResult, SerializedError } from 'src/createAsyncThunk'
+
+import apiRequest, { AxiosError } from 'axios'
+import { IsAny } from 'src/tsHelpers'
 
 function expectType<T>(t: T) {
   return t
@@ -25,7 +27,7 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
       })
       .addCase(async.rejected, (_, action) => {
         expectType<ReturnType<typeof async['rejected']>>(action)
-        expectType<Error>(action.error)
+        expectType<Partial<Error> | undefined>(action.error)
       })
   )
 
@@ -98,3 +100,84 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
   // typings:expect-error
   defaultDispatch(fetchBooksTAC(1))
 })()
+/**
+ * returning a rejected action from the promise creator is possible
+ */
+;(async () => {
+  type ReturnValue = { data: 'success' }
+  type RejectValue = { data: 'error' }
+
+  const fetchBooksTAC = createAsyncThunk<
+    ReturnValue,
+    number,
+    {
+      rejectValue: RejectValue
+    }
+  >('books/fetch', async (arg, { rejectWithValue }) => {
+    return rejectWithValue({ data: 'error' })
+  })
+
+  const returned = await defaultDispatch(fetchBooksTAC(1))
+  if (fetchBooksTAC.rejected.match(returned)) {
+    expectType<undefined | RejectValue>(returned.payload)
+    expectType<RejectValue>(returned.payload!)
+  } else {
+    expectType<ReturnValue>(returned.payload)
+  }
+})()
+
+{
+  interface Item {
+    name: string
+  }
+
+  interface ErrorFromServer {
+    error: string
+  }
+
+  interface CallsResponse {
+    data: Item[]
+  }
+
+  const fetchLiveCallsError = createAsyncThunk<
+    Item[],
+    string,
+    {
+      rejectValue: ErrorFromServer
+    }
+  >('calls/fetchLiveCalls', async (organizationId, { rejectWithValue }) => {
+    try {
+      const result = await apiRequest.get<CallsResponse>(
+        `organizations/${organizationId}/calls/live/iwill404`
+      )
+      return result.data.data
+    } catch (err) {
+      let error: AxiosError<ErrorFromServer> = err // cast for access to AxiosError properties
+      if (!error.response) {
+        // let it be handled as any other unknown error
+        throw err
+      }
+      return rejectWithValue(error.response && error.response.data)
+    }
+  })
+
+  defaultDispatch(fetchLiveCallsError('asd')).then(result => {
+    if (fetchLiveCallsError.fulfilled.match(result)) {
+      //success
+      expectType<ReturnType<typeof fetchLiveCallsError['fulfilled']>>(result)
+      expectType<Item[]>(result.payload)
+    } else {
+      expectType<ReturnType<typeof fetchLiveCallsError['rejected']>>(result)
+      if (result.payload) {
+        // rejected with value
+        expectType<ErrorFromServer>(result.payload)
+      } else {
+        // rejected by throw
+        expectType<undefined>(result.payload)
+        expectType<SerializedError>(result.error)
+        // typings:expect-error
+        expectType<IsAny<typeof result['error'], true, false>>(true)
+      }
+    }
+  })
+}