-
Notifications
You must be signed in to change notification settings - Fork 110
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
Rethrow apollo errors and recommend error boundaries #28
Comments
Forcing an ErrorBoundary around every component seems painful at first glance. When let result
try {
const {data} = useQuery(MY_QUERY);
result = data;
} catch(e) {
if (isPromise(e)) { throw e; }
result = makeErrorData(e)
}
return (<Component {…result} />); |
I'd make it optional (but enabled by default) so if you wanted the old behavior you could use it like e. g. But the general idea is to make error management similar to what suspense does for data loading. So in most cases, it should be enough to put a single error boundary at the top of the React tree, but if you need it, you can also put it at the bottom, just above a component which requires a special behavior. Please compare: render(
<Suspense fallback={<GlobalLoadingFallback />}>
<MyPageComponent>
<PageDataComponent />
<Suspense fallback={<ImageLoadingFallback />}>
<MyImageComponent />
</Suspense>
</MyPageComponent>
</Suspense>
); and: // a helper component but you can also easily implement it by yourself
import { ErrorBoundary } from 'react-error-boundary';
render(
<ErrorBoundary FallbackComponent={GlobalError}>
<MyPageComponent>
<PageDataComponent />
<ErrorBoundary FallbackComponent={ImageError}>
<MyImageComponent />
</ErrorBoundary>
</MyPageComponent>
</ErrorBoundary>
); Of course, you can mix suspense and error boundaries. Please also note that you have access to the error instance inside of the error boundaries. |
#34 also related to this, because we have to merge array of GraphQL errors in to single |
I came across this issue because I'm trying to use The idea of using exceptions for this, inspired by suspense, seems like it might be workable. Though I'm not thrilled with the magic and implicit requirement to wrap components in a In fact I don't think even this would be enough for my use-case, in my case the subscription really is optional, depending on whether the data from the query includes some specific fields. The subscription simply isn't needed in some scenarios. Perhaps I actually need a @trojanowski have you had any more thoughts about this issue since December? |
Hah, I just wanted to open a new issue to propose exactly this. I definitely support this. Even though it's possible to do it with custom hooks, it sounds like a good idea to have it by default. Btw, it's not really necessary to wrap every component in the error boundary. That's the beauty of it. You can have a single boundary on top of the app. Then there might be some left panel which can render errors differently, but another part of the app is still working. There can be "Retry" button which would essentially re-mount those components and re-run queries to try it again. 😍 I believe this is what React is trying to teach us with Context and Suspense. Instead of thinking about atomic components, we should be thinking about features and how to present failures to a user the best way. Frankly, I haven't tried this in real world yet, but I definitely want to! |
I think it would make sense to have two separate hooks long term (when Suspense for data fetching will be considered stable): // This hook uses both suspense and error boundaries, so we can be sure here
// that `data` in the returned object is not null and has a correct type.
useHighLevelQuery<TData, TVariables>(query, options): {
data: TData,
// ...
}
// Like in the current version of `useQuery` - we can't be sure if `data`
// is already loaded (or if a network error happened) so it can be undefined.
// Also, it's possible that there were errors in GraphQL resolvers, so a correct
// type for data, in that case, should be something like RecursivePartial:
// https://stackoverflow.com/a/51365037/2750114
useLowLevelQuery<TData, TVariables>(query, options): {
data?: RecursivePartial<TData>;
loading: boolean;
// a network error (or also a GraphQL error since v0.4.0)
error?: ApolloError;
// GraphQL errors (thrown by server resolvers)
errors?: GraphQLError[];
// ...
} Of course, the actual hook names would be different. In the short term, I'll add a |
Hm, but throwing errors are not related to Suspense, it's about error boundaries which are stable. So I would stick to the original plan and make it default :) And btw, I tried to use Otherwise, I agree with high/low hooks, I already got something like that implemented because I was annoyed by checking for data and if it contains actual data. It works quite nicely. const { data, loading, error, ...rest } = Hooks.useQuery<TData, TVariables>(query, options)
if (error) {
throw error
}
return {
loading: loading || !data || isEmpty(data),
data: data as TData,
...rest,
} |
They are related to me that way that they work best combined. I don't see too much benefit in that code: const Hello = () => {
const { data, loading } = useQuery(GET_HELLO);
if (error) return <Loading />;
return <p>{data.hello.world}</p>;
};
render(
<ErrorBoundary>
<Hello />
</ErrorBoundary>
); vs const Hello = () => {
const { data, error, loading } = useQuery(GET_HELLO);
if(!dataNotAvailable({ data, error, loading }) {
return <FallbackContent error={error} loading={loading} />
}
return <p>{data.hello.world}</p>;
};
render(<Hello />);
When I wrote the original plan, I hadn't thought I'd change the default value of the
Good catch. I remember discussions about So I plan to introduce a new option ( |
@trojanowski I have a feeling you haven't used error boundaries too much yet so you are kinda biased against them :) I don't agree that handling errors is the same as handling loading state. Usually, you want some way to recover from the error. Be it reloading the whole app to avoid cascading errors or even re-rendering only part of the tree which has failed. And that's exactly where error boundaries shine. Really think of them as "feature recovery container". Under the "feature" term you can imagine a whole app, some kind of product listing or even a button that does some kind of validation. In case of the error, you can show the fallback UI which can be about user filling some kind of feedback or really a "retry" button which would re-execute all queries. Think about that last statement: The error boundary can re-execute all contained queries for you. If you would be handling errors manually in every component, you also have to manually call What's also important to mention, you can have a super simple error boundary which would only log the error, but keep UI untouched and then you can really do a granular work of handling errors on query level if you like. class ErrorBoundary extends React.Component {
componentDidCatch(error) {
console.error(error)
}
render() {
return this.props.children;
}
} We could expose such boundary directly in this package just for convenience. Besides, in my rewrite of the |
I use them and like them. Please notice that I opened this issue :)
It's the opposite of my experience. I usually try to have only one query per page with the help of fragment collocation. However, there are cases when I have more than one query. But there is always a "main" one - e.g. which is used to server-render the page, and there are "less important" components with own queries, which are invoked after client JS bundle is loaded. I definitely wouldn't like to show a fallback container on the whole page or re-fetch the main query if the less important one failed.
I suggested something else. Please look at #93 (comment). |
So I'd like support for error boundaries enabled by default, but not yet. As I said in #28 (comment) I'd like to have two hooks - the high-level one recommended for most cases (which will use suspense and error boundaries) and the second one - when you prefer to manage everything by yourself. |
Funny, we are doing quite an opposite to support modularization. Each part of the app can stand on its own without relying on something. Besides, fragments have a big flaw as they don't support arguments. We need that very often and I cannot imagine having a huge query that accepts tens of unrelated variables. Perhaps, I am missing something there, but it doesn't matter. Point being for matters of this issue that there are vastly different approaches and every default will probably annoy someone :) |
@trojanowski, I am not able to understand your original proposal. As far as I can tell, you are proposing that the following code could handle errors properly by introducing an Error Boundary at a higher level in the component tree.
My understanding is that Error boundaries do not catch errors from asynchronous code, which is what the useQuery hook is. So an error thrown by that hook wouldn't be caught by the Error Boundary anyway. What am I missing here? Can you please explain. |
@nareshbhatia You can throw error synchronously even from const useQuery = () => {
const [error, setError] = React.useState(null)
if (error) {
throw error
}
} |
Thanks @FredyC. I tried this approach in my code and it seems to work well. I am seeing my ErrorBoundary catch the error and display the error message. The only glitch is that after all this happens the component is being re-rendered, I don't know why. This causes the useQuery to be called again and another error being thrown. This time the error is not being caught by the error boundary and the app crashes. I will take a look at this tomorrow, but if you have any insights please let me know. |
That's the behavior of ErrorBoundary unfortunately. There is no way to prevent re-render, it's by design to avoid "broken UI". Basically, you should render some replacement UI which informs about the error. It can have "retry" button to try again or possibly a "restart" button to reboot the whole app. |
Thanks @FredyC, I got it to work! Here's the hook and here's the ErrorBoundary. Note that my use case is REST, but the idea for handling errors is the same. |
FYI, I am having mixed luck with throwing an error synchronously and having the
It seems that the interactions between thrown errors and error boundaries are very finicky. So far my workaround is not to throw synchronous errors, but expose them as part of the API. The client component can check for these errors explicitly and show error messages. This seems to be much more stable. |
Well, generally you are better without actually throwing errors :) It's a quick & dirty way, but also kinda error-prone (pun intended). I would say the ErrorBoundary should be considered more like a safety net, not the general way of handling errors. A much better way, in my opinion, is to utilize the Context. You can simply expose yourself a <ApolloDefender
onNetworkError={onNetworkError}
onUserErrors={onUserErrors}
onOperationError={onOperationError}
>
<SillyErrorBoundary onError={onUnhandledError}>
{render()}
</SillyErrorBoundary>
</ApolloDefender> Note: ApolloDefender is proprietary for now This is also a reason why I wanted to wrap the errors with additional information in #93, but @trojanowski rejected without giving it much of the thought seemingly. I have a custom fork that has it and it's amazing. In the ApolloDefender I can see all information about a failed operation without any awkwardness. |
@FredyC, thanks for your insights. I am failing to understand how Context would help in this case. While an |
Well, I think that throwing known errors can be simply unpredictable. You would need to take care of edge cases and ensure nothing is left hanging. It's like you would take a stab into the component and if it's not ready for it, it will crumble. It's extremely hard to ensure everything is solid. The Context is a completely different mechanism that won't do anything bad to the component. Instead, you will decide if current UI should stay there or be properly cleaned up. |
Thanks, @FredyC, that does give me a better "context" (pun intended) :-) |
@FredyC - Would you mind sharing the |
@amcdnl Well, there is nothing magical about it, it's just bunch of conditions about recognizing of what type of error it is and calling the appropriate callback with it. It builds on top of ApolloOperationError to supply more details about the error. And there is a Context which provides I don't want to share it because it also contains a bunch of specific code to our projects and backend solution that would be only confusing if presented. Sorry. |
Right now errors are returned as a part of the
useQuery
result. Maybe a better idea would be to throw them and recommend to use Error Boundaries instead (at least in the suspense mode).So instead of this code:
we could write:
and use an error boundary at the root of the react tree (or anywhere else above the component) - similar to how we use the
<Suspense />
component.The text was updated successfully, but these errors were encountered: