'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:

  1. 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. 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

  3. 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