reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.56k stars 1.14k forks source link

How to customize error type for queries and mutations? #2942

Closed progtarek closed 7 months ago

progtarek commented 1 year ago

Versions used

"@reduxjs/toolkit": "^1.9.0",
"react-redux": "^8.0.2",

I am trying to add general type support for the incoming (server side) errors After investigating I found this link https://github.com/rtk-incubator/rtk-query/issues/86

As I understood here https://github.com/rtk-incubator/rtk-query/issues/86#issuecomment-738845312 this is something essentially dictated by the fetchQuery

and this is my implementation so far

type CustomizedFetchBaseQueryError = {
  message?: string
  errors?: { [key: string]: string }
}

export const apiSlice = createApi({
  reducerPath: 'corporateAPI',
  baseQuery: <BaseQueryFn<string | FetchArgs, unknown, CustomizedFetchBaseQueryError, {}>>(
    fetchBaseQuery({
      baseUrl: import.meta.env.VITE_BASE_URL,
    })
  ),
  endpoints: () => ({}),
})

Although it shows that the incoming type is const [createUser, { error, isError, isLoading }] = useCreateUserMutation() const error: SerializedRemoteError | SerializedError | undefined trying to access a key inside the customized type giving me this error

Property 'errors' does not exist on type 'SerializedRemoteError | SerializedError'.
  Property 'errors' does not exist on type 'SerializedError'.ts(2339)

Screenshot from 2022-11-24 12-39-35

phryneas commented 1 year ago

There is always the chance that "some other error" happens, e.g. if you throw something in a queryFn. That would end up being a SerializedError. Please take a look at type safe error handling

nubpro commented 1 year ago

@phryneas I'm stuck for a few hours trying to figure out how to type the errors correctly. I've read the docs but I still don't quite get it. All my endpoints throw the same shape of error, such as below:

{
    "message": "OTP not found or expired or invalid",
    "errors": {
        "otp": [
            "OTP not found or expired or invalid"
        ]
    }
}

interface ApiErrorResponse {
  message: string;
  errors: {[k: string]: string[]}
}

Are we supposed to follow what the OP did which is to cast the custom error interface at createApi? Or we have to cast it manually on every mutation?

 const [verifyOtp, result] = useVerifyOtpMutation();

try {
   await verifyOtp();
} catch (err) {
   if ('data' in err) {
          const customError = err as ApiErrorResponse; // Is this right? And we have to repeat this on every mutation?
          console.log(customError.message);
   }
}
phryneas commented 1 year ago

@nubpro that looks like a very non-standard err object you have there - is that actually in that shape on runtime? What code have you written for that?

nubpro commented 1 year ago

@nubpro that looks like a very non-standard err object you have there - is that actually in that shape on runtime? What code have you written for that?

Our backend is actually written on Laravel. Hence, we are folllowing their standard of practise with their API.

On a side note, throwing custom errors using React Query + Axios is quite straightfoward. Let me show you a contrived example of what I mean:

// note: this is a different type than the one I shown previously
type ServerResponse<T = void> = {
    status: string;
    message: string | null;
    data?: T;
};

// Take a look below "AxiosError<ServerResponse>"
const { status, error, data } = useQuery<ServerResponse<DataReferences>, AxiosError<ServerResponse>>(
        ['getConsultantFormReferences'],
        () => axiosInstance.get('/api/consultant/getReferences').then(res => res.data)
    );

As you can see, I can easily wrap their useQuery with a custom error type to align with my expected server error's response. Hence, I'm wondering if the same can be done on RTK Query?

phryneas commented 1 year ago

@nubpro all that assumes that an error is "working" though.

But that's against the nature of an error. An error is something unexpected. What if a reverse proxy somewhere along the line is not working and injects its own content? What if the virus scanner on the user's computer changes something? What if the server is misconfigured and returns only garbled content? And then, the more obvious ones: what if the user's internet is down, the connection is interrupted, or something along those lines? In those cases, you don't even have data to work with.

Simply casting error.data to have a certain value will be just wrong in those cases. There is a reason why an error in a catch block in TypeScript is always unknown. And we go for a similarly defensive approach: An error is (if you are using fetchBaseQuery) either a FetchBaseQueryError (if the error is kinda known and could be handled correctly if the baseQuery) or a SerializedError (if something completely unexpected happened, like your query function crashing). At that point, realistically you can still not make any assumptions on the shape of error.data - you have to test if the shape what it is, for example with a zod schema.

So I would write a zod schema for "a FetchBaseQueryError with a certain type of data attached" and use that in your component. It's the only safe way to know that the error is really what you expect it to be.

nubpro commented 1 year ago

@nubpro all that assumes that an error is "working" though.

But that's against the nature of an error. An error is something unexpected. What if a reverse proxy somewhere along the line is not working and injects its own content? What if the virus scanner on the user's computer changes something? What if the server is misconfigured and returns only garbled content? And then, the more obvious ones: what if the user's internet is down, the connection is interrupted, or something along those lines? In those cases, you don't even have data to work with.

Simply casting error.data to have a certain value will be just wrong in those cases. There is a reason why an error in a catch block in TypeScript is always unknown. And we go for a similarly defensive approach: An error is (if you are using fetchBaseQuery) either a FetchBaseQueryError (if the error is kinda known and could be handled correctly if the baseQuery) or a SerializedError (if something completely unexpected happened, like your query function crashing). At that point, realistically you can still not make any assumptions on the shape of error.data - you have to test if the shape what it is, for example with a zod schema.

So I would write a zod schema for "a FetchBaseQueryError with a certain type of data attached" and use that in your component. It's the only safe way to know that the error is really what you expect it to be.

Thanks for the detail insight. With that said, I have created a helper that would determine whether the exception error is coming from the server (I called it ApiResponse) or fetchBaseQueryError or unknown as you have said. By simply checking whether the status returned is a number or not, if it is, I'm sure that it is from the server.

Helpers:

export interface ApiErrorResponse {
  status: number;
  data: { message: string; errors: { [k: string]: string[] } };
}

export function isApiResponse(error: unknown): error is ApiErrorResponse {
  return (
    typeof error === "object" &&
    error != null &&
    "status" in error &&
    typeof (error as any).status === "number"
  );
}

In practise:

  try {
    await sendOtp().unwrap();
  } catch (error) {
    if (isApiResponse(error)) {
      // present error to the user
      alert(error.data.message)
    } else {
      // log error
      console.error(error);
    }
  }

What do you think of this way?

phryneas commented 1 year ago

Yes, that is very close to this example from the docs

nubpro commented 1 year ago

Yup, I did reference the code there and modified to suit my needs. Thanks again, Lenz, you were a big help!


From: Lenz Weber-Tronic @.> Sent: Friday, December 2, 2022 3:10:12 AM To: reduxjs/redux-toolkit @.> Cc: Chai @.>; Mention @.> Subject: Re: [reduxjs/redux-toolkit] How to customize error type for queries and mutations? (Issue #2942)

Yes, that is very close to this example from the docshttps://redux-toolkit.js.org/rtk-query/usage-with-typescript#inline-error-handling-example

— Reply to this email directly, view it on GitHubhttps://github.com/reduxjs/redux-toolkit/issues/2942#issuecomment-1334226095, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAF2IIRSZ6ZHUBTG5FJ7LTLWLDZZJANCNFSM6AAAAAASKJROEM. You are receiving this because you were mentioned.Message ID: @.***>