redux-toolkit: Accessing the original error in createAsyncThunk
With the current implementation of createAsyncThunk
, there is no way to access any errors returned from a server. Here is a specific scenario which I imagine is extremely common:
You attempt a form submission with a set of data like POST /users
with a body of { first_name: 'valid', last_name: 'valid', email: 'in@valid' }
and the API responds with a 400:
{
"details": [
{
"message": "\"email\" must be a valid email",
"path": [
"email"
],
"type": "string.email",
"context": {
"value": "in@valid",
"key": "email",
"label": "email"
}
}
],
"source": "payload",
"meta": {
"id": "1582509238404:1c50114e-ef8e-4d97-b3e4-dbc995e8fcff:20:k6zpbe70:10030",
"api": "1.0.0",
"env": "production"
}
}
In this case, I need some way to access that error so that I could update my UI - for example, by unwrapping the details
field and using Formik’s setErrors
to provide feedback. I imagine it’d also be valuable for detailed toasts as well, where people useEffect
and pop an alert if the error message changes.
After looking through createAsyncThunk
, the only real solution I can think of is
- Passing a new parameter for
rethrow
and rethrowing the error instead of returningfinalAction
.
If we were able to rethrow, we could do this sort of thing:
<Form
onSubmit={async (values, helpers) => {
try {
await dispatch(myAsyncThunk(values));
} catch (err) {
const joiErrors = err.response.details;
if (joiErrors) {
helpers.setErrors(unwrapErrors(joiErrors));
}
}
}}
/>
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 9
- Comments: 18 (8 by maintainers)
Ok, just for future Googler’s who can’t seem to figure out how to access the original response body. I’ve created a helper
api
class (as follows)just so that I can easily create async thunks like this
and refer to the originally returned JSON in my
extraReducers
as followsNow,
state.error
will contain the JSON that was returned by my API. Hope this helps someone.Thank you very much for fixing it in a better way. But I took the following method to make the url change for the axios method and the choice for params, queryString more convenient.
users.js
api.js
@phryneas Thanks for the insight!
No, not really - we could dispatch an
addUser()
action after the success, or asetErrorMessage
on the failure in the event we - but IMO this is more tedious and eliminates the point of acreateAsyncThunk
abstraction.I might not be understanding correctly, but if you
return
ed the error in the payloadCreator after catching it, you’d then be doing things like this, which seems like a more confusing approach?My other thought matches up with yours and I’d be happy to open a PR with that as well:
I think this is probably the best approach. My only additional feedback there is we might want to consider forcing a standardized error format when throwing a custom error object - the current format looks good, but we’d want to add a key such as
data
. In my example, that would look like this:The benefit of this being an established pattern that offers predictable handling inside of all reducers -
[whatever.rejected]: (state, action) => { state.error = action.payload.error }
would always have the given shape. I could also see the counterpoint where this library shouldn’t care about if a developer wants to dump an error with no context into the error message and we just serialize it away - but I think allowing that to happen in thedata
field is a good compromise.Based on the example from @infostreams this is what I came up with … just incase it helps someone else.
UPDATED per the suggestions from @markerikson. Thank you!
Hi, I updated the code in my comment so that you can now also properly use parameters in GET and DELETE requests. There’s even support for named parameters, so your URL can be something like “/post/get/:id” and it will replace the :id with the appropriate value.
You can pass these values when dispatching the action, for example
@jmuela1015 two quick suggestions syntactically:
extraReducers
: https://redux-toolkit.js.org/api/createSlice#the-extrareducers-builder-callback-notationcreateSlice
uses Immer, you can “mutate” the array withstate.errors.push()
instead ofstate.errors = [...state.errors]
how pass parameters ??
export const requestPasswordReset = createAsyncThunk('login/requestReset', api.post('/password/email'))
Hmm…
This makes me question two things:
If you go to the lengths to even do exception handling from your component - are you even using the global store here? Does Redux make your life more easy here, or is it just a burden? Do you maybe even throw away the submission result (beyond the errors), so nothing of meaning would ever be stored in your redux store?
Sure, for the user it’s an error. But for you as a programmer, that seems more like a result that you are maybe even expecting on more than 50% the responses. Is this still an error? Or wouldn’t it make more sense to reserve the error/rejected way for things like connection timeouts etc. and handling this as a part of the “fulfilled” route? I mean, the
fetch
was a success, it’s just not the result the user wants to see, but for your app it’s a pretty normal state, not really an “exception”. So you could justreturn
this from the payloadCreator.@markerikson
I’d rather not go down this road. In TypeScript, just extending Error will give you a class that does not really extend error. You’ve got to do extra magic to get a class that extends Error from the engine viewpoint. So this is a very unreliable check for anything.
We could allow the
payloadCreator
to return/throw own fulfilled/rejected actions (checkisFSA
andactionCreator.fulfilled.match/reject
on the return value/error) and let those override our finalAction. For those, we could just assume that they are serializable.We could even think about bailing from miniSerializeError from conditions like
err.constructor === Object
or!(/(error|exception)/i.test(err.constructor.name))
- as this will probably be developer-provided data. If that isn’t serializable, the serializability check will warn the developer in dev mode about it.Per DMs, my inclination at this point is that if you need to reformat anything from the server, it’s your job to do it in the payload creator.
Axios requests where you need to grab something out of
response.data
, normalization of entity data, extracting important values from a server error… we don’t know anything about that ourselves. Do it yourself in the payload creator, and whatever you return or throw will end up in the corresponding action.