Redux-toolkit: catch error from createAsyncThunk

Created on 24 Apr 2020  路  7Comments  路  Source: reduxjs/redux-toolkit

So lets say I have a thunk, which I created as follows:

export const login = createAsyncThunk(
  'auth/login',
  async (credentials: Credentials): Promise<User> => {
    return authClient.login(credentials.username, credentials.password);
  }
);

What I usually do is in my codebase when dispatching thunks:

async function handleClick() {
  try {
    await dispatch(login(credentials))
  } catch (err) {
    showToastForError(err)
  }
}

However createAsyncThunk() does not throw the error if it occurred (instead it uses it to dispatch the rejected action). Is there some way I can achieve this? or what would be the alternative.

Also when using typescript the result of awaiting dispatch(login(credentials)) returns an action object, but it does not have an error property which I could use to check if an error occurred. This seems just to be a typings issue since logging the actual object does show the error property exists

Most helpful comment

All 7 comments

We were facing the same issue regarding toasts in our project. We solved the issue by creating a wrapper for the async logic inside createAsyncThunk which in a simplified version looks as follows:

export function withToastForError<Args, Returned>(payloadCreator: (args: Args) => Promise<Returned>) {
    return async (args: Args) => {
        try {
            return await payloadCreator(args);
        } catch (err) {
            showToastForError(err);
            throw err; // throw error so createAsyncThunk will dispatch '/rejected'-action
        }
    };
}

In the above example, one would define the thunk as follows:

export const login = createAsyncThunk(
    'auth/login',
    withToastForError(
        async (credentials: Credentials): Promise<User> => {
            return authClient.login(credentials.username, credentials.password);
        },
    ),
);

and in the component

const handleClick = () => dispatch(login(credentials));
export const login = createAsyncThunk(
    'auth/login',
    withToastForError(
        async (credentials: Credentials): Promise<User> => {
            return authClient.login(credentials.username, credentials.password);
        },
    ),
);

Could you explain how to access thunkAPI in this example? Sorry I can't get it to work @mhienle

Accessing thunkAPI is one of those things I excluded in the above example (like I said, it's a simplified version 馃槈).

It is important to note that withToastForError returns the same thing that it takes as parameter, i.e. a function (args: Args) => Promise<Returned>. It basically returns a modified or "decorated" version of payloadCreator (modified in the sense that depending on whether executing payloadCreator throws an error or not it performs side effects like showing toasts).

As you see from the login-example, what we want to be able to do is passing the return value of withToastForError as the second parameter to createAsyncThunk, so withToastForError and its types need to make this possible. Therefore (since withToastForError returns the same type as its parameter), withToastForError does not require (payloadCreator: (args: Args) => Promise<Returned>) as parameter, but instead requires exactly what createAsyncThunk requires as second parameter and that is AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>, a type that exposes thunkAPI.

It looks something like this (still simplified though):

export function withToastForError<Args, Returned, ThunkApiConfig>(payloadCreator: AsyncThunkPayloadCreator<Returned, Args, ThunkApiConfig>) {
    return async (args, thunkAPI) => {
        try {
            return await payloadCreator(args, thunkAPI);
        } catch (err) {
            showToastForError(err);
            throw err; // throw error so createAsyncThunk will dispatch '/rejected'-action
        }
    };
}

export const login = createAsyncThunk(
    'auth/login',
    withToastForError(
        async (credentials: Credentials, thunkAPI): Promise<User> => {
            // now Typescript should let me use thunkAPI here
            return authClient.login(credentials.username, credentials.password);
        },
    ),
);

The version we use in our project basically does the same thing, but it allows more configuration, e.g. showing toasts in case of success and a configurable text in the toast that uses the original args, error (in case of failure) or the returned value of payloadCreator (in case of success).

Thanks a lot @mhienle that you took the time to answer my question in such great detail. I'm feeling much more confident right now thanks to you. Thank you :-).

@mhienle, you had an elegant solution. But why do you want to put your UI render logic inside createAsyncThunk. Should you just have your State to store the error. Then have your main component to check error state there?

@muz3 since toasts are often timing-related and managed from start to end by third-party libraries, I think it can be totally okay to view them as a "side effect" rather than "ui-specific logic" ;)

Was this page helpful?
0 / 5 - 0 ratings