-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Accessing the original error in createAsyncThunk #390
Comments
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 |
Hmm. After further conversation, I'm still not convinced that "rethrowing" is the right idea. However, if the payload creator extracts some kind of an object from the error and tries to rethrow that, Can we do an |
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'd rather not go down this road. In TypeScript, just extending Error will give you a class that does not really extend error. We could allow the We could even think about bailing from miniSerializeError from conditions like |
@phryneas Thanks for the insight!
No, not really - we could dispatch an
I might not be understanding correctly, but if you [updateUserThunk.fulfilled]: (state, { payload }) => {
// we would need to force a check here, always, as we could be returning "an error"
if (payload.user) {
state.users = upsert(payload.user)
} 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 export const axiosUpdateThunk = createAsyncThunk(
"users/update",
async ({ data }) => {
try {
const result = await axios.put(`api/users`, data);
return result.data;
} catch (err) {
const customError = {
name: "Custom axios error",
message: err.response.statusText,
data: err.response.data // serializable
};
throw customError;
}
}
); The benefit of this being an established pattern that offers predictable handling inside of all reducers - |
If you actually use those actions dispatched from
You're gonna have that distinction either in the |
Gotcha. I'm a fan of throwing custom error messages as shown, as it plays nicely with both selecting well-defined errors from the store, as well as having the option of As a side note, if we were to go with that mindset of matching the |
You do what works best for you there, no question! And yes, there seems to be a disconnect between fetch and other libraries - I don't really think one is better than the other, as both require you to do very specific things. Completly new train of thought: What is a bit of a shame here is that we cannot really provide any type information for the error of the If we would think my suggestion from above further:
In that case, we could allow devs to specify a generic type for This might be the most TS-compatible way of handling this. If you still want to |
I think that makes a lot of sense. The typing on that would be nice as we'd just be able to do something like I did some experimenting with all of the ideas that are outlined here, and from a usability standpoint, handling success/'errors' in |
So what's the consensus out of that discussion, if any? |
I was experimenting with a lot of different ways to implement the types on this earlier and didn't come up with anything notable. I'm going to keep at it and see if I can get something to pan out. |
This was addressed in #393 |
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 const handleRequest = (request, thunkApi) =>
request
.then(response => response.data)
.catch(error => thunkApi.rejectWithValue(error?.response?.data || error))
const buildUrl = (url, params) => {
// Support URLs with named identifiers, such as '/posts/get/:id'.
// This code replaces the ':id' part with the value of params.id
Object.keys(params).forEach(k => {
if (url.indexOf(':' + k) > -1) {
url = url.replace(':' + k, params[k])
delete params[k]
}
})
// all the parameters that were not bound to a named identifier are appended to the URL
const encoded = Object.entries(params).map(kv => kv.map(encodeURIComponent).join("=")).join("&")
return url + (encoded.length > 0 ? '?' + encoded : '')
}
export default class api {
static post = (url, axiosConfig = {}) => (obj = {}, thunkApi) =>
handleRequest(axios.post(url, obj, axiosConfig), thunkApi)
static get = (url, axiosConfig = {}) => (obj = {}, thunkApi) =>
handleRequest(axios.get(buildUrl(url, obj), axiosConfig), thunkApi)
static delete = (url, axiosConfig = {}) => (obj = {}, thunkApi) =>
handleRequest(axios.delete(buildUrl(url, obj), axiosConfig), thunkApi)
} just so that I can easily create async thunks like this import {createAsyncThunk} from '@reduxjs/toolkit'
import api from "service/api"
export const requestPasswordReset = createAsyncThunk('login/requestReset', api.post('/password/email')) and refer to the originally returned JSON in my [requestPasswordReset.rejected]: (state, action, more) => {
state.loading = 'error'
state.error = action?.payload
}, Now, |
how pass parameters ??
|
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 this.props.dispatch(requestPasswordReset({username:"someone@example.com"}) |
Thank you very much for fixing it in a better way. users.js import {createAsyncThunk} from '@reduxjs/toolkit'
import {http, thunkHandler} from '../utils/api';
export const signInUser = createAsyncThunk(
'users/signInUser',
(user, thunkAPI) => thunkHandler(http.post('/users/signIn', user), thunkAPI);
) api.js export const http = axios.create({
baseURL: apiUrl,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
});
export const thunkHandler = async (asyncFn, thunkAPI) => {
try {
const response = await asyncFn;
return response.data.result;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data);
}
}; |
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! import {createAsyncThunk, createSlice} from '@reduxjs/toolkit'
import axios from "axios";
export const signInUser = createAsyncThunk(
'users/signInUser',
async (user, thunkApi) => {
return axios.post('/users/signIn', user)
.then(response => response.data.results)
.catch(error => thunkApi.rejectWithValue(error?.response?.data || error))
}
)
export const usersSlice = createSlice({
name: 'users',
initialState: { user: {}, errors: [] },
reducers: {},
extraReducers: builder => {
builder
.addCase(signInUser.pending, state => {
state.errors = []
})
.addCase(signInUser.fulfilled, (state, action) => {
state.errors = [];
state.user = action.payload;
})
.addCase(signInUser.rejected, (state, {payload}) => {
switch (payload?.response?.status) {
case 401:
state.errors.push({ error: "Access denied." }); break;
case 403:
state.errors.push({ error: "Forbidden." }); break;
default:
state.errors.push(payload); break;
}
})
}
});
export const { } = usersSlice.actions;
export default usersSlice.reducer; |
@jmuela1015 two quick suggestions syntactically:
|
Based on @lqqokim solution
|
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: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'ssetErrors
to provide feedback. I imagine it'd also be valuable for detailed toasts as well, where peopleuseEffect
and pop an alert if the error message changes.After looking through
createAsyncThunk
, the only real solution I can think of isrethrow
and rethrowing the error instead of returningfinalAction
.If we were able to rethrow, we could do this sort of thing:
The text was updated successfully, but these errors were encountered: