'React Error Boundaries not working properly with async function
So basically I wait for the Auth to return the current authenticated user from aws amplify/cognito, then I run a query to get their data(I intentionally have it erroring out so I can check the functionality of the Error Boundary in this situation). I import client.ts in app.tsx and call the getData function passing in the parameters to it, and then return the axios error object to the calling function. I have run into a few issues and haven't been able to get any of them to work properly with this as explained below:
- If I set the "throw" in the data[0].error conditional, it gives me uncaught Promise since I don't reject() the Promise in there manually. When I try and do that I get some typescript error about not being able to do that because Component doesn't have that type of something or other(I will redo and post the actual error)
2) If I set the throw in the render function with a conditional based on state.hasError, it works and hits the error boundary but then throws an error because I have an uncaught exception because I am not catching that error I am throwing
If I catch the error that is thrown in the render function, it hits the error boundary but then complains that render() is not returning anything
So looking for advice on to how to improve this from a design standpoint the issue I am having with all the examples I find on this is that they are simple examples where the async call is made directly in the component instead and the errors are handled directly in the component which I don't want to really do, as I like not having to repeat code over and over again when I can simply pass it to a function that resides in one place to return what I want.
App.tsx:
async componentDidMount() {
await Auth.currentAuthenticatedUser().then( data => {
this.onGetAuthUser(data.username);
var username = data.username;
//rockset query --need to implement error handling elsewhere
getData('select * from "ruzzleLeagueCollection" WHERE username=:username',
[{"name": "username",
"type": "string",
"value": data.username}]).then((data: any) => {
if(data[0] != null) {
if(data[0].error != null) {
this.setState({error: true, errorData: data[0].error});
return;
}else {
//get the logged in user.
this.onGetLoggedUser(data[0]);
}
}else {
//create the loggedUser object and pass it
this.onGetLoggedUser([{
username: username,
ruzzleName: null,
group: 0,
hasProfile: false,
currentSkill: null,
highestSkill: null
}])
}
});
});
}
render() {
try{
if(this.state.error) throw this.state.errorData;
return (
<div >
<Router>
<div>
<NavBar/>
<Switch>
<Route exact path="/" component={Home}/>}/>
<Route path="/players" component={Players} />
<Route path="/season" component={Season} />
<Route path="/records" component={Records} />
<Route path="/stats" component={Stats}/>
<Route path="/admin" component={Admin}/>
</Switch>
</div>
</Router>
</div>
)
} catch(err) {
console.log('error caught!');
}
}
client.ts
import axios from 'axios';
const apiKey = 'fgf' //process.env.REACT_APP_ROCKSET_APIKEY;
const apiServer = process.env.REACT_APP_ROCKSET_APISERVER;
const url = apiServer + '/v1/orgs/self/queries';
const headers = { headers: {'Authorization': 'ApiKey ' + apiKey}};
var data = {'sql':{
'query': '',
'parameters': [
]}
}
//GET REQUEST
export async function getData(query: string, parameters: any) {
data.sql.query = query;
data.sql.parameters = parameters == null? undefined: parameters;
try {
const response = await axios.post(url, data, headers)
return response.data.results;
}catch(err) {
console.error('The following error occurred ' + err.message)
if(err.response) {
return handleErrors(err.response, 'response');
}else if(err.request) {
return handleErrors(err.request, 'request');
}else {
return handleErrors(err, 'other');
}
}
}
function handleErrors(error: any, errorType: string) {
switch(errorType) {
//this is a response error
case 'response':
return [{error: {
data: error.data,
status: error.status,
headers: error.headers
}
}]
case 'request':
return [{ error:
{ data: error.request}
}]
default:
return [{error: {
data: error.message}
}]
}
}
Errors.tsx:
export class Errors extends Component<any,IState>{
constructor(props:any){
super(props);
this.state = {
hasError: false,
alertOpen: true,
error: null,
info: null
};
}
componentDidCatch(e: any, info: any) {
this.setState({
hasError: true,
error: e,
info: info
});
}
closeAlert() {
this.setState({alertopen: false, hasError: false})
}
//render the error screen
render() {
if(this.state.hasError){
var msg = 'Oops! Something went wrong. Error: ' + this.state.info.componentStack;
var title = 'Error in ' + this.state.error.data.message + ' ' + this.state.error.status;
return(
<div>
<React.Fragment>
<AlertDialogSlide
open={this.state.alertOpen}
title = {title}
message= {msg}
closeAlert={() => this.closeAlert()}
error={true}
>
</AlertDialogSlide>
</React.Fragment>
</div>
)
}
return this.props.children;
}
}
index.js(entry point) --Error Boundary wrapping
ReactDOM.render(
<Provider store={store}>
<Errors>
<App />
</Errors>
</Provider>
, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers:
serviceWorker.unregister();
Solution 1:[1]
As stated in the react docs: Error boundaries do not catch errors for asynchronous code:
Error boundaries do not catch errors for:
- Event handlers
- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
- Server side rendering
- Errors thrown in the error boundary itself (rather than its children)
Solution 2:[2]
There is this little hack that I found on https://medium.com/trabe/catching-asynchronous-errors-in-react-using-error-boundaries-5e8a5fd7b971
It worked for me so why not...
import { useCallback, useState } from "react";
/** A workaround for error boundary async limitations. */
export const useAsyncThrow = () => {
const [_, setErr] = useState();
return useCallback(
(e) =>
setErr(() => {
throw e;
}),
[setErr]
);
};
// Example:
// const asyncThrow = useAsyncThrow;
// (async () => asyncThrow(new Error()))();
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Daniel PĂ©rez |
Solution 2 | Davoodeh |