Canonical way to handle exceptions in async functions in solid-js?

61 views Asked by At

I'm very new to solid and frontend development hence my question. I have a class that talks to server API. It uses cookie to authenticate so eventually cookie may expire and an endpoint returns http 401 error.

export class UserActions {

  private handleError(response: Response): void {
    if (response.status === 401) {
      useNavigate()(`/login?redirect=${encodeURIComponent(location.pathname)}`)
      // throw new ServiceError(ErrorType.Authentication, 'Please login again');
    }
  }

  login(email: string, password: string): Promise<User | null> {

    return fetch('http://localhost:8080/api/login', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
      }),
    })
        .then(response => {
          this.handleError(response);
          return response.ok ? response.json() : null;
        })
  }
}

and here is example how it's used

export default function UserProfile() {
  const context = useContext(SessionContext);

  const [read, write] = createSignal<string>("Initial value")

  const clickHandler = async () => {
    let user = await api.currentUser()
    write(user.email)
  };

  return (
      <AuthenticatedRoute>
        <div>Hello, {context?.user?.firstName} {context?.user?.lastName}</div><br/>
        <button onClick={clickHandler}>Refresh</button><br/>
        <div>Value: {read()}</div>
      </AuthenticatedRoute>
  );
}

How can I centralize handing of such error so that any page that gets the exception would redirect user to login page ? I tried using but it doesn't catch exception thrown in onClick handler because, I assume, it's not thrown in rendering stage.

I tried calling navigate() in UserActions but it does nothing.

What is the proper way to achieve such redirect ? Thank you.

3

There are 3 answers

0
expert On BEST ANSWER

Apparently I have to wrap async calls into createResource to make its exceptions to be catchable in ErrorBoundary.fallback.

That was my page become something like this

export default function UserProfile() {
  const context = useContext(SessionContext);

  const [data, { mutate, refetch }] = createResource(() => api.currentUser())

  return (
      <AuthenticatedRoute>
        <div>Hello, {context?.user?.firstName} {context?.user?.lastName}</div><br/>
        <button onClick={refetch}>Refresh</button><br/>
        <Suspense>
          <div>Value: {data()?.email}</div>
        </Suspense>
      </AuthenticatedRoute>
  );
}
1
Charlie On

You can use async/await to handle asynchronicity and use JS exception handling around that.

async login(email: string, password: string): Promise<User | null> {


   try {
      var response = await fetch('http://localhost:8080/api/login', {
          method: 'POST',
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            email: email,
            password: password,
          }),
      })
  }
  catch(e){
      this.handleError(response);
  }

  return response.ok ? response.json() : null;
}
1
snnsnn On

Solid runs synchronously and can handle synchronous errors only but it does not mean that we can not handle asynchronous errors. There are multiple ways to do it, I will try to explain one by one from most basic to the more idiomatic way:

  1. You can create a signal that tracks the state of the ongoing request, and update the signal accordingly.
 interface State {
    status: 'pending' | 'resolved' | 'rejected';
    data?: any;
    error?: any;
};

const [state, setState] = createSignal<State>({ status: 'pending' });

const handleClick = () => {
  getUsers()
    .then(data => setState({ status: 'resolved', data, error: undefined }))
    .catch(error => setState({ status: 'rejected', error }));
}

If the request fails, you can store the error and render it.

Here you can find a detailed description of this process: https://stackoverflow.com/a/74590574/7134134

If you don't want to render the error, or do not want to handle them one by one, you can throw it, so that the closest ErrorBoundary will catch it.

As you might guess, this signal allows us to convert an async error to a synchronous one.

  1. Instead of creating your own signal, you can use the resource API.

The way resource API is quite similar to the previous solution, only difference is that it integrates with the Suspense API and has a few more utilities like supporting hydration and mutation etc.

Resource API is enough to fetch and render a remote data. It is enough to handle its errors too:

function App() {
  const [data] = createResource(fetchData);

  return (
    <div>
      <Switch fallback={<div>Not Found</div>}>
        <Match when={data.state === 'pending' || data.state === 'unresolved'}>
          Loading...
        </Match>
        <Match when={data.state === 'ready'}>
          Display Data
        </Match>
        <Match when={data.state === 'errored'}>
          Display error message
        </Match>
      </Switch>
    </div>
  );
}
  1. The third solution is using a resource for fetching data and a Suspense component for rendering it.

Suspense deals with the loading state and the rendering of the data, leaving the error handling to the ErrorBoundary. Basically it will throw the error like I mentioned in solution #1 so that it can be caught by the closest error boundary.

Now, about authentication. It is a bit complex topic and totally depends on your application logic, if it is SPA or SSR. But in the end it all boils down to conditional execution. From the UI perspective it is also just a conditional rendering. The hardest part is not to implement it but doing it securely.

If is is SPA, you can use OAuth, if it is SSR, you can use traditional authentication mechanism like cookies. React has abundant examples which could be adapted to SolidJS easily.