Customize GraphQL Errors with React useQuery Hook
In this lesson, we'll address how we can handle the loading and error state of our GraphQL queries with our custom useQuery Hook.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL with a single-time purchase.

[00:00 - 00:12] Though our use query hook is in a decent place, some of the important things we haven't taken into account yet were to track the loading and error states of our server fetch method. We'll first address the loading state of our request.
[00:13 - 00:27] When we say loading, we're essentially referring to being able to track the status of our asynchronous request. If it's in flight, the UI should reflect this with a loading indicator of sorts , and when complete we should be presented with the expected data.
[00:28 - 00:43] To keep track of loading, we'll introduce a new loading field into the state interface of our custom hook and declare its type as boolean. We'll then initialize this loading value as false in our state initialization.
[00:44 - 00:55] At the beginning of the fetch API method, we'll set the loading state to true. We'll also specify that data is still null.
[00:56 - 01:12] When our request is successful, we'll set loading back to false. We've already used the spread syntax to return everything within state, so all that's left to do is to destruct the loading property from our hook in the component.
[01:13 - 01:34] In the listings component, we'll simply extract the loading property from the use query hook, and if loading is ever true, we'll have our component simply render a header tag that says "loading". When loading is set back to false, we'd present the expected UI to the user, which is the list of listings and the title.
[01:35 - 01:47] Now when we head to the browser, we'll notice this brief loading message when the request is in flight. To better see this, we can even change our browser's network status to a slower status like slow3g for example.
[01:48 - 02:08] Now let's address what would happen if our server fetch function was to error out. With Apollo server and GraphQL, errors are a little interesting.
[02:09 - 02:18] Often times when a query has failed and returns an error, the server may treat the query request as successful. We can see an example of this.
[02:19 - 02:39] If we head to the listings field resolver, in our server app code, here we're at the listing resolver's file we've set up, let's assume we wanted to throw an error at the beginning of our resolver function. When we head back to our client and attempt to query listings, we're not going to get the information we want.
[02:40 - 03:01] If we take a look at our network tab, find the post API request bait, we can see that the request made to /api, the GraphQL endpoint, was successful with status code 200 . However, if we take a look at the response from the API request, we can see that data is null and an errors array is populated.
[03:02 - 03:22] When an error is thrown in a Apollo server, this error gets populated inside the errors array where extensions is an object that contains information about the error added by a Apollo server. From that note then, in our server fetch function, we should specify that the response can also return an errors field as well.
[03:23 - 03:36] For the sake of simplicity, we're going to create a very simple interface for this error. If we take a look at the response again, we can see that errors is an array that has a few different properties for each error object to provide different information.
[03:37 - 03:48] We're going to keep this simple and we'll assume that we're only interested in the message property. So we'll create an interface label error that simply has a message string field .
[03:49 - 04:07] And in the return statement of our server fetch function, we'll state that an errors field can also be returned with which its type is array of error. To be on the safe side, we should also guard for when the fetch response code itself is not successful.
[04:08 - 04:22] The window fetch function behaves uniquely when this happens because it doesn't actually throw an error and it also appears to be resolved. It however provides us with a response.ok field that dictates if the response has been made successful.
[04:23 - 04:38] We'll use the response.ok field to check if the response status code is ever not 200 and throw an error if it is. We'll give it a message that just says failed to fetch from server.
[04:39 - 04:59] Just to reiterate once again, the response.ok check is used to check for if the server response ever returns a status that is not successful. The returned errors field from our response is for if a request is successful but our GraphQL API returns an error within an errors field for some reason or another.
[05:00 - 05:12] Let's now head back to our use query hook and handle if the server function is to return an error. First we'll introduce a new field to our state interface with the label ever.
[05:13 - 05:35] For the sake of simplicity we won't try to pass any error messages from the server function to the component itself and we'll let the component handle how it was to display the errors. As the results will simply declare an error property of type boolean and we'll initialize our error state property with the value of false.
[05:36 - 05:48] We'll wrap the insides of our Fetch API function within a try and catch statement. The catch statement will be used to catch any response status errors.
[05:49 - 06:06] If a request error is to occur within our server fetch function, we'll want to set our state by having data be set to null, loading to false and the error field to true. We'll also want to surface this error in the browser console.
[06:07 - 06:25] We'll look to surface whatever error message arises by retrieving the error in our catch block and using the console error function that's responsible in logging errors to the console. We'll also specify this console error function in a throw to prevent any further execution of code.
[06:26 - 06:42] As a side note, if we try to specify the type of the error parameter here, we 'll notice that TypeScript tells us that the catch clause variables may not have a type annotation. The reason behind this is that there's really no way to know what type an exception will have.
[06:43 - 07:01] They could be thrown objects, system generated exceptions and so on. Conveniently, the console error function expects a parameter of type any and consumes either a key value object, a string or a much more traditional error object.
[07:02 - 07:19] We'll need to now ensure the other state properties in our existing set state functions are also set accordingly. For now, let's just say in both functions, we'll just set the error properties to false.
[07:20 - 07:25] Now let's see if our catch statement does as intended. We'll head back to the server code.
[07:26 - 07:55] We'll remove the forced throw in the listings resolver and we'll actually now just exit our server completely. If we head back to the client and try to launch the app, we'll be presented with the error we expect in our console because now our server fetch request has failed completely.
[07:56 - 08:31] Though we haven't handled the capture and display of error in our UI yet, the one other thing we need to consider is if the server fetch function is successful, but the GraphQL API actually returns error as part of the errors array. Here is where we can destruct the errors field from our server fetch function, check for its length, in other words, check for its presence, and if it exists, we can throw an error so the catch statement can then run and set the console warning as well as the error state field to true.
[08:32 - 08:47] Even though errors from our GraphQL API comes back as an array, for the sake of simplicity, we'll assume one error is usually thrown at a time, so the array would most likely only be populated with a single object. This is just for simplicity sake.
[08:48 - 09:00] For a more robust solution, you would probably come up with a better way of doing this. However, in this case, we can just pass in the message from the first object in our array to the thrown error function.
[09:01 - 09:22] Our server error message from the API will now be caught and pass to our console error message. If we force an error to be thrown in our server, we'll notice the actual error message in our console.
[09:23 - 09:37] We're now capturing errors in one of two cases. In the first case, the actual request could fail, in the second case, the request could have a status code of 200, but the API response could contain errors.
[09:38 - 09:56] Though we're doing this for both cases, we haven't actually surfaced any information in our UI. So in the listings component, we'll destruct the error field, we'll check for its presence, and if it's ever true, we'll have our components return an error message only.
[09:57 - 10:10] We'll say something like, "Uh-oh, something went wrong, please try again later ." At this moment, we still have the thrown error in our listings resolver function.
[10:11 - 10:35] So in our UI, when we try to fetch listings, we can see that even though the fetch request could have had a status code of 200, we capture the error that the API sends back, and we notify the user that something went wrong. This would work just the same if our server fetch request completely erred out, and we had an error status code in our request.
[10:36 - 10:55] We could go back to the listings resolver, remove the forced thrown error, and we can simply try to exit the server and try to fetch some information on the client. We'll be presented with the same message in our UI, despite the fact that the error at this point is due to something else.
[10:56 - 11:14] We'll restart the server once again, and we'll look to refesh the listings to verify that we're able to do so when there are no errors. And we'll also see the very brief loading sign, which tells us that we're also keeping track of the loading status of the asynchronous request as well.
[11:15 - 11:31] If we delete a listing, we'll see the loading indicator once again, since the refetch function actually sets the loading state value back to true, just before the asynchronous request is about to be made. Okay, we'll stop here for now.
[11:32 - 11:49] In our component, we can notice that we're actually still making a custom fetch function where we intend to run the delete mutation. How about we think about extrapolating this functionality here, though not as extrapolated as the one in use query, to a custom use mutation hook.
[11:50 - 11:50] And we'll do this in the next lesson.