Closed joenoon closed 5 years ago
The point of calling resetStore
is you basically want to flush out information from the store and bring it to a clearly known state. If you have queries in flight and one of them comes back, writing the result to the cache may either cause an error (leading to a broken UI component) or lead to a state that's not expected by the application developer after calling resetStore
(e.g. may leak PII).
I think throwing an error on having an inflight query is actually the intended outcome here. However, the part where you're seeing [undefined]
for promises feels like a bug. Could you write a quick unit test that produces this behavior and we can then work on getting it fixed?
Is it possible to display the query name causing this error? Error message is not helpful, it is difficult to fix this error.
@Poincare If no one else has noticed the undefined promise, then it's probably something specific to my app which is using vue-apollo. I'll try to think up how I could repro this in isolation, but not much comes to mind.
I'm still not clear why throwing an error is the intended outcome the way it seems to work now.
I could see if there was a way to handle the error, but aren't the promises getting rejected just floating around internally in apollo-client and detached from userland-access?
Or I could see if there was a way to first stop inflight queries purposely, and then safely call resetStore.
But I'm just not wrapping my head around calling something that may or may not throw an exception with no way to catch it. If I'm missing something obvious, then my apologies.
I believe the correct approach is the stop inflight queries at the application level and then call resetStore
. The operation that resetStore
represents is certainly not safe with inflight queries, so although I can imagine some cases where you might want to catch the error and proceed on with the reset anyway, it probably isn't a great practice.
I'm not sure if I completely grok the problem. I imagine that you do see the error "Store reset while query was in flight" in your application - is this error not possible to catch?
is this issue related to this one? https://github.com/apollographql/apollo-link-state/issues/198
In my case, I was running into this and needed a way to wait for in-flight queries to resolve. It worked pretty well in my case because I have wrappers around calling refetch
for my observable queries, so all I really did was the following:
const inflightPromises = [];
function refetch(variables) {
const refetchPromise = myObservable.refetch(variables).then(results => {
// you could use native array methods here too
// just remove the promise from the array
_.remove(inflightPromises, p => p === refetchPromise);
return results;
});
inflightPromises.push(refetchPromise);
return refetchPromise;
}
function reset() {
// Promise.all does not resolve with empty arrays
const waitFor = inflightPromises.length
? Promise.all(inflightPromises)
: Promise.resolve();
// maybe also guard against calling this while waiting
return waitFor.then(() => apolloClient.resetStore());
}
@Poincare When using react-apollo, the state of inflight queries is an implementation detail. Should this problem be reported there?
I think the correct solution would be to wait for all pending queries to finalize, meanwhile holding any new incoming queries, resetting the store, and then running the pending queries. This is very hard to do at any level other than the client, no?
My use case is that when the user changes the language, I want to refetch everything. The "better/more efficient" alternative is passing the current language to every query, which is a lot of work for a pretty rare event.
There are a bunch more people with this issue at #1945…
That said, it seems that in my case the issue is solved by resetting the store at the same time as setting the new language state instead of after the language state was set.
@Poincare
The operation that resetStore represents is certainly not safe with inflight queries, so although I can imagine some cases where you might want to catch the error and proceed on with the reset anyway, it probably isn't a great practice.
I'm hitting the error again in another place, and I can't work around it. I'm having a hard time imagining a situation where you call client.resetStore()
and then you don't want in-flight queries to be reset as well. Can you elaborate?
Not sure this helps anyone in this thread but I didn't think to see if resetStore
was a Promise and turns out it is. For me it was an issue of the requests that were happening right after resetStore
- not the already in flight ones (I think). So changing this
cookies.set('jwt', response_data.data.getCreds.token)
this.props.client.resetStore()
this.props.loggedIn(response_data)
to this
cookies.set('jwt', response_data.data.getCreds.token)
this.props.client.resetStore().then(() => {
this.props.loggedIn(response_data)
})
got rid of the error
@joenoon I created a reproducible to what could be the same issue, but it's not clear to me if it's actually the same issue or not. Could you take a look at #3555 and confirm if the issue is a duplicate?
I agree with the sentiment that in-flight queries should be automatically dropped and refetched instead of throwing errors (or at least, that behavior should be available as an option).
Here's my situation: after completing a mutation with lots of side effects, I would like to drop the entire cache (in lieu of support for targeted cache invalidation) and then navigate to the next page.
If I call await client.resetStore(); history.push(nextPage);
, the current page will visibly refetch all of its data, the app will wait for this to finish, and then navigate to the next page. This is not desirable because the app waits for data that isn't needed anymore to finish loading, and it creates visual noise as the previous page goes into its loading state, then shows its refetched data for a split second before navigating to the next page, which will also show a loading state before settling with the latest data.
If I instead reverse the process to history.push(nextPage); await client.resetStore();
, I get the error shown here. I get the same result if I don't wait for the store to finish resetting before navigating (client.resetStore(); history.push(nextPage);
).
@dallonf If you want to drop entire cache without triggering reload of current page queries you can achieve that by calling client.cache.reset()
instead of client.resetStore()
. E.g. call reset()
on Apollo Cache directly somehow.
@vlasenko Ah, thanks, that's definitely an easier workaround than what I was going for (forcing an empty render while resetting the store), although this still definitely needs to be addressed. Relying on an undocumented internal function shouldn't be the official solution to any problem!
@dallonf I'm not from official team, just a user. I understand that calling client.cache.reset()
is not nice, thats why I added:
E.g. call reset() on Apollo Cache directly somehow.
If you have access to Apollo Cache somehow, you can call reset()
directly on cache instance, this is an official way to reset it, I believe, without relying on internals.
same issue.
Here is my code snippet:
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(error => {
// console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
if (error.code === 1001) {
auth.signout();
//client.resetStore()
client.cache.reset();
window.location.replace('#/login');
//client.resetStore()
// .then(result => {
// window.location.replace('#/login');
// })
// .catch(err => {
// console.log('reset store error: ', err);
// });
}
});
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
if I ignore handling the promise client.resetStore()
method returned. It will throw Store reset while query was in flight.
error.
And, I have another issue. If graphQLErrors
happened, client.resetStore().then().catch()
will always trigger catch
and dead loop.
This issue is related to https://github.com/apollographql/apollo-client/issues/3555, https://github.com/apollographql/apollo-client/issues/3766 and https://github.com/apollographql/apollo-client/issues/3792. We'll address them all in one shot.
@Poincare I don't understand your point "I believe the correct approach is the stop inflight queries at the application level and then call resetStore
" - can you elaborate on how we could track all inflight queries at the application level so that we could stop them? The whole point of Query
as I understand it is to make the use of GraphQL declarative, so our code should have no idea what queries are running at any one point and it would be extremely difficult to track all running queries ourselves since they get fired off (at GraphQL's discretion) based on when Query
elements are mounted ... So how could a user track these queries? I can't see an easy or reliable way of doing that but can you suggest one? :-)
I think the most reliable and consistent approach is for the Apollo code to stop all queries when resetStore is invoked. Otherwise signout and signin of users is a lot trickier than it needs to be.
Thoughts? I'm open to all feedback and thanks for all of your hard work on GraphQL and Apollo. :-)
OK, the way I solved this in the short term is to just totally blow away/delete/GC the apollo client when the user signs out and create a whole new apollo client when they sign in. That seems to have fixed things.
It also seems a reasonable thing to do since when the user signs out they are effectively saying "get rid of my data on this browser because I'm done using this app or another user wants to sign in" so getting rid of the whole client, while it feels clunky, does a reasonable job of deleting all the data. It is less desirable for the case where a JWT token has just expired and the user needs to re-login, but maybe we can find a better solution for that case?
Here's some sample code (not heavily tested, experimental), posting it in case it might help somebody else:
//
// App.js
//
import React, { Component } from "react";
import { ApolloProvider, Query } from "react-apollo";
import "./App.css";
import AuthStatus from "./components/AuthStatus";
import { init } from "./graphql/Client";
function setupApollo() {
const authTokenKey = "authToken";
const authTokenProvider = {
get: () => localStorage.getItem(authTokenKey),
set: authToken => localStorage.setItem(authTokenKey, authToken),
remove: () => localStorage.removeItem(authTokenKey),
};
const apollo = init({
uri: "http://localhost:4000/graphql",
authTokenProvider,
});
return { authTokenProvider, apollo };
}
class App extends Component {
state = { authTokenProvider: null, apollo: null };
logout = () => {
const { authTokenProvider } = this.state;
if (!authTokenProvider) {
return;
}
authTokenProvider.remove();
const newApollo = setupApollo();
this.setState({ ...newApollo });
};
login = () => {
const { authTokenProvider } = this.state;
if (!authTokenProvider) {
return;
}
const authToken = "from your server after successful sign in ...";
authTokenProvider.set(authToken);
const newApollo = setupApollo();
this.setState({ ...newApollo });
};
componentDidMount() {
const newApollo = setupApollo();
this.setState({ ...newApollo });
}
renderIsLoggedIn() {
// There would be much more to render here normally, keeping this short as an example ...
return <p>Logged in!</p>
}
renderIsNotLoggedIn() {
// There would be much more to render here normally, keeping this short as an example ...
return <p>Logged out!</p>
}
render() {
const { apollo } = this.state;
if (!apollo) {
console.log("empty app render");
return null;
}
return (
<ApolloProvider client={apollo.client}>
<AuthStatus>
{({ status }) =>
status === "loggedIn"
? this.renderIsLoggedIn()
: this.renderIsNotLoggedIn()
}
</AuthStatus>
</ApolloProvider>
);
}
}
export default App;
//
// AuthStatus.js
//
import PropTypes from "prop-types";
import React from "react";
import { Query } from "react-apollo";
import { getAuthStatus } from "../graphql/Queries";
function AuthStatus(props) {
const { children } = props;
return (
<Query query={getAuthStatus}>
{({ loading, error, data }) => {
if (loading) return <p>Loading ...</p>;
if (error) return <p>Error: {error.message}</p>;
if (data && data.authStatus) {
return children({ status: data.authStatus.status });
} else {
return null;
}
}}
</Query>
);
}
AuthStatus.propTypes = {
children: PropTypes.func.isRequired,
};
export default AuthStatus;
//
// Queries.js
//
import gql from "graphql-tag";
export const getAuthStatus = gql`
query AuthStatus {
authStatus @client {
id
status
}
}
`;
//
// Client.js
//
import ApolloClient from "apollo-boost";
function isUnauthenticatedError(err) {
if (!err) {
return false;
}
if (/You must be signed in/i.test(err.message)) {
return true;
}
if (err.extensions && err.extensions.code === "UNAUTHENTICATED") {
return true;
}
return false;
}
export const init = ({ uri, authTokenProvider }) => {
const authStatusData = () => ({
authStatus: {
id: "authStatusKey",
__typename: "authStatus",
status: authTokenProvider.get() ? "loggedIn" : "loggedOut",
},
});
const client = new ApolloClient({
uri,
clientState: {
resolvers: {
Query: {
authStatus: () => {
return authStatusData().authStatus;
},
},
},
},
request: operation => {
const authToken = authTokenProvider.get();
if (authToken) {
operation.setContext({
headers: {
authorization: `Bearer ${authToken}`,
},
});
}
},
onError: ({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(err => {
if (isUnauthenticatedError(err)) {
authTokenProvider.remove();
const newData = { data: authStatusData() };
client.cache.writeData(newData);
}
});
}
},
});
return { client };
};
@joelgetaction Why just not call reset()
on the instance of Apollo Cache when you need to sign out the user and drop the cache? You are creating something like InMemoryCache
is it right? Just call reset()
on the instance of InMemoryCache
. I think it is the simplest approach.
@vlasenko thanks for the reply. Just resetting the cache didn't seem to work well. There were weird visual artifacts and I was still getting the error about inflight requests. I agree that your inflight suggestion would be simpler, but it didn't seem to work reliably ... And also, I need something to reset the authStatus and resetCache didn't handle that.
Is there a cost to blowing away and recreating the apollo client like I'm doing? I mean my React app seems very fast even when doing that - instrumenting, it doesn't seem to take longer than 1 ms so is there an advantage to resetCache other than it might be simpler?
Thanks again for your help!
@joelgetaction We are using cache.reset()
:
https://github.com/sysgears/apollo-universal-starter-kit/blob/2fb6562f02db4a68705fd047b2b85b56f00fe5b1/packages/client/src/modules/user/access/index.js#L11
It works reliably for us. We don't have error about inflight request. If you have this error it means you are still calling resetStore()
somewhere. Just remove these calls. Use only cache.reset()
I'm not sure about the cost of recreating apollo client. I do not think there is some advantage in calling reset() on the cache directly over recreating apollo client, except the former is just simpler. And as I said we are using approach with cache.reset()
for months already in production and we haven't seen reliability issues with it.
Another tip, as we use JWT and auth as well. You can add a hook for all network errors on the client. When you detect a 401, you can immediately handle the logout/login flow.
I didn't think if doing a cache.reset() we might add that too.
On Fri, Nov 2, 2018, 02:49 Victor Vlasenko notifications@github.com wrote:
@joelgetaction https://github.com/joelgetaction We are using cache.reset():
https://github.com/sysgears/apollo-universal-starter-kit/blob/2fb6562f02db4a68705fd047b2b85b56f00fe5b1/packages/client/src/modules/user/access/index.js#L11 It works reliably for us. We don't have error about inflight request. If you have this error it means you are still calling resetStore() somewhere. Just remove these calls. Use only cache.reset()
I'm not sure about the cost of recreating apollo client. I do not think there is some advantage in calling reset() on the cache directly over recreating apollo client, except the former is just simpler. And as I said we are using approach with cache.reset() for months already in production and we haven't seen reliability issues with it.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/apollographql/apollo-client/issues/2919#issuecomment-435298190, or mute the thread https://github.com/notifications/unsubscribe-auth/AAiaa2dOtmAy5zazSXw0iffdP8VXs0pxks5uq_kLgaJpZM4Rvful .
Not sure if this is relevant to all the use cases here but this flow is working ok for me:
login = async ({ token, user, authenticated }) => {
const {
props: { client }
} = this;
if (authenticated) {
localStorage.setItem("token", token);
await client.resetStore();
this.setState({ user });
}
return null;
};
logout = async () => {
const {
props: { client }
} = this;
localStorage.setItem("token", "");
this.setState({ user: null, token: null });
await client.resetStore();
};
@joshdcuneo your solution works as you're storing "login" state in the React component state which is not a case in most examples presented here. The problem occurs when you try to store "login" state in the apollo cache which is the most obvious place to store it. I want to have access to it from any place in the app.
It's more and more frustrating to use Apollo Client. With Redux I didn't have such kind of problems. I'm really considering getting back to Redux for local state management. I will prepare reproduction repository tomorrow to show that this is still an issue
I've created simplest possible reproduction repository here: https://github.com/lukejagodzinski/apollo-client-reset-store-bug
Error is thrown on each sign out (client.resetStore()
). I'm not doing any fancy things here just one server query (when signed in) and one mutation of the client cache for storing sign in status. I think that here https://github.com/apollographql/apollo-client/blob/a444e58cea27e14ce4c4f2053b0322dcd532d0fa/packages/apollo-client/src/core/QueryManager.ts#L827 error shouldn't be thrown if promises were rejected because of the store being reset.
To my surprise it take really long time to resolve those promises. Even when result of the query was already displayed the promise related with this query is still being processed? Maybe it some other bug in the code.
I've also recorded short screencast showing a bug https://youtu.be/hY_JiLApXzk
I was just having the same issue. After some experimentation, including some fixes listed above (to no avail,) I ended up making my logout function synchronous and it got rid of the error.
No Error:
signoutUser: () => {
//Remove token in localStorage
localStorage.setItem("token", "");
//End Apollo Client Session
apolloClient.resetStore();
}
Error:
signoutUser: async () => {
//Remove token in localStorage
localStorage.setItem("token", "");
//End Apollo Client Session
await apolloClient.resetStore();
}
I don't get why I was getting this error because I have the onError property for my ApolloClient instance set which seems to work in catching all other Apollo errors???
//Catches all errors
onError: ({
operation,
graphQLErrors,
networkError
}) => {
//operations object not currently in use, but stores query/mutation data the error occurred on
if (networkError) Vue.toasted.show(networkError.message);
if (graphQLErrors) {
for (let err of graphQLErrors) {
Vue.toasted.show(err.message);
}
}
}```
Hi, not sure if it is worth opening a new issue for this, but sometimes resetStore()
neither resolve or fails. This is very hard to reproduce, it only happens when I run my app in a popup window, and I log out. Then, when I sign in again, the promise eventually throws a "store reset when query in flight" error.
Any idea what could cause resetStore()
to wait indefinitely ? Could it be related to some page reload, an invalid URL or a query that fails silently ?
Edit: the reset store error also seems to happen for no reason, even when I don't call resetStore(). Could SSR provoke this error too ?
@eric-burel I think I ran into something similar, but was too lazy to open issue or create minimal reproduction. Here is what happened for me:
Query
components A
(outer) and B
(inner)A
and B
are renderedA
is rendered, but B
is not.resetStore
resetStore
calls refetch
on all mounted queries . Both A
and B
are still mounted, so refetch is called on both. A
and B
are resolved is arbitraryB
gets resolved first, everything is fineA
gets resolved first, B
gets unmounted, and it's query promise gets stuck somewhere in limbo of QueryManager
B
gets unmounted before resetStore
during logout.@doomsower very interesting, I will dig that later but thank you very much for the insight. This might be related indeed, as the view changes on logout even if the resetStore() is not done. It also happens on login (without logout), which also triggers some redirect.
Edit: this has been solved in Vulcan by @ErikDakoda : https://github.com/VulcanJS/Vulcan/pull/2313. Instead of relying on a Promise, the solution for us was to use the new onResetStore
callback of the Apollo client.
This way, the callback is not registered in the logout React component, which may be erratically dismounted depended on your workflow (eg with an optimistic UI), but at the Apollo client level. So it is preserved and run even after an amount. This issue has disappeared so far and the code feels more robust.
Thanks for reporting this. There hasn't been any activity here in quite some time, so we'll close this issue for now. If this is still a problem (using a modern version of Apollo Client), please let us know. Thanks!
Still happening on stable as of two weeks ago
The way I see it, resetStore
should:
resetStore
call until the reset is finishedAnd do the following in order:
Is there any case where this wouldn't be appropriate?
In-progress Query components don't need to know that the store is resetting; from their perspective it can just look like the query took from before the reset to after the reset to finish.
Queries that were finished before the reset, and subsequently need to be refetched, can just jump from ready
state back to loading
state -- or does this violate any critical assumptions?
Subscriptions don't need to see a status change due to the reset either. From their perspective it can look like they're still subscribed and there just haven't been any events.
@vlasenko resetting only the cache seems risky unless you're stopping all of your in-flight queries at application level. Queries that were in-flight before the reset (e.g. queries for the previous logged-in user) could write invalid results to the reset cache when finished.
Then restart all pre-existing queries and subscriptions without causing them to error
@jedwards1211 a very common use case is to reset the store on logout, if the pre-reset queries are restarted, they will either fail or resolve with invalidated parameters (Authorization
header of the not-logged-anymore user).
I don't know how you're putting in the auth header but in my case when the queries are restarted, the middleware to add headers gets called anew and injects the new user's auth header
Running into this issue now. Try all the solutions above, but seems not work. Anyone have some news about this issue ?
Same here, and I am using @apollo/client 3.0.0-beta.43
I can't reproduce this anymore with @apollo/client 3.0.0-beta.46.
It's happening in my project for modern apollo
├── @apollo/react-hoc@3.1.3
├── @apollo/react-hooks@3.1.3
├── @apollo/react-ssr@3.1.3
├── apollo-cache@1.3.4
├── apollo-cache-inmemory@1.6.5
├── apollo-client@2.6.8
├── apollo-link@1.2.13
├── apollo-link-batch-http@1.2.13
├── apollo-link-error@1.1.12
├── apollo-upload-client@12.1.0
├── apollo-utilities@1.3.3
Reported error
Error: Network error: Store reset while query was in flight (not completed in link chain)
at new ApolloError (bundle.esm.js:63)
at ObservableQuery.push.../../node_modules/apollo-client/bundle.esm.js.ObservableQuery.getCurrentResult (bundle.esm.js:159)
at QueryData.push.../../node_modules/@apollo/react-hooks/lib/react-hooks.esm.js.QueryData.getQueryResult (react-hooks.esm.js:265)
at QueryData._this.getExecuteResult (react-hooks.esm.js:73)
at QueryData.push.../../node_modules/@apollo/react-hooks/lib/react-hooks.esm.js.QueryData.execute (react-hooks.esm.js:106)
at react-hooks.esm.js:380
at useDeepMemo (react-hooks.esm.js:354)
at useBaseQuery (react-hooks.esm.js:380)
at useQuery (react-hooks.esm.js:397)
at Query (react-components.esm.js:8) "
at Meta (http://localhost:8080/common.chunk.js:104011:3) // <------------ This is my component
at Query (http://localhost:8080/app.chunk.js:167:26)
So basically what I'm trying to achieve is to reset apollo in-memory cache on redux store change when user logged out, it may be because user clicked "Logout" link intentionally or token expired etc.
After "logout" user is automatically redirected to another page that run some graphql queries about the page (head meta data like title, description etc.).
I'm not sure how should I call client.clearStore()
from store.subscribe
or redux middleware
in a way that it won't conflict with any new or pending queries. Is there a way to safely schedule clear of apollo's cache?
Is something like below (https://github.com/apollographql/apollo-client/issues/3766#issuecomment-619829099) the way to fix it?
client.stop()
client.clearStore()
I can't reproduce this anymore with @apollo/client 3.0.0-beta.46.
Just open react-development-tools, find ApolloProvider and exec
for (let i = 0; i< 3; i++) {
console.log('i', i);
setTimeout($r.props.client.resetStore, 10)
}
I still got error
Unhandled Runtime Error
Invariant Violation: Store reset while query was in flight (not completed in link chain)
Before, in pure js i just checking by
if(!client.queryManager.fetchCancelFns.size) {
await client.resetStore()
}
but i white in typescript now and client.queryManager is private https://github.com/apollographql/apollo-client/blob/main/src/core/ApolloClient.ts#L74
It's so sad! I can't check this any more and apollo does not provide any checking(((
UPD. But i can check like this
client["queryManager"].fetchCancelFns.size
and no typescript errors...
Calling client.stop() method before clearStore solved my problem.
client.stop() client.clearStore();
Accroding the official doc: Call this method to terminate any active client processes, making it safe to dispose of this ApolloClient instance. https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.stop
Calling client.stop() method before clearStore solved my problem.
client.stop() client.clearStore();
Accroding the official doc: Call this method to terminate any active client processes, making it safe to dispose of this ApolloClient instance. https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.stop
@khushahal-sharma it's bot optimal. For example, i have many subscriptions and got many events for updating at the moment. In this case starts many cicles "Send query, stop, send another...". Checking global sending status more useful.
Calling client.stop() method before clearStore solved my problem.
client.stop() client.clearStore();
Accroding the official doc: Call this method to terminate any active client processes, making it safe to dispose of this ApolloClient instance. https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.stop
still encounter this error with "@apollo/client": "~3.4.17". so we will adopt this solution for now.
Intended outcome:
resetStore
without errors being thrown.https://github.com/apollographql/apollo-client/blob/b354cf07e979def7aa0219e60dca63e8a33fa822/packages/apollo-client/src/core/QueryManager.ts#L787..L796
Actual outcome:
I'm running into an issue when calling
resetStore
after a logout scenario. I think I understand the reasoning, but in my case I can't seem to find a workable way around it, and something else odd is happening:fetchQueryPromises
in snippet above is aMap
, with values of{promise,reject,resolve}
, so I look inside to see what they are:const promises = Array.from(this.fetchQueryPromises.values()).map(x => x.promise)
My plan was to
await Promise.all(promises)
before callingresetStore
, however,promises
is[undefined]
. For some reason, an entry is added tofetchQueryPromises
without a promise, so I can't wait for it before resetting.The actual error
Store reset while query was in flight.
looks like it might not actually cause a problem, which was my main concern. But it seems like there should be a way to avoid an error being thrown.Waiting for existing promises to complete before reset, or exposing a function to allow the user to await them before resetting seems right to me.
Is there a case for calling
resetStore
where a thrown error is the right outcome?Version