11/ How to handle errors in the NextAuth authorize function / CredentialsProvider
Tags: nextauth, nextjs, credentialsprovider
Published: 2024-04-01 16:28:24

All the code for this chapter is available on [github: branch credentialssignin](https://github.com/peterlidee/NNAS/tree/credentialssignin).

The app in it's current state actually works, as long as you enter the correct email/username + password. But, we haven't done any error handling. So, what happens when we enter an incorrect password? Let's try it out, we run the app and enter an incorrect password.

```
http://localhost:3000/authError?error=Cannot%20read%20properties%20of%20undefined%20(reading%20%27username%27)
```

We are redirect to the `/authError` route with an error searchParam: 'Cannot read properties of undefined (reading username)'.

What is going on here? Firstly, the `/authError` page is the custom `NextAuth` page that we setup in an [earlier chapter](https://dev.to/peterlidee/how-to-add-error-handling-to-nextauth-googleprovider-1903). `NextAuth` only uses it in limited cases. This case seems to be one of those. Of course, this is not a good way to give our user some feedback. We will be handling these error ourself so this custom `NextAuth` error page won't get shown anymore. Still, it's nice to see that `NextAuth` has our back.

The error 'Cannot read properties of undefined (reading username)' comes from the `authorize` function. The line where we return the user to be exact:

```jsx
return {
  name: data.user.username,
  //...
};
```

Inside `authorize` we make a request to `Strapi`, passing our incorrect credentials. `Strapi` says nope and returns a strapiError object:

```ts
// frontend/src/types/strapi/StrapiError.d.ts
export type StrapiErrorT = {
  data: null;
  error: {
    status: number;
    name: string;
    message: string;
  };
};
```

Data is null. So, when we are trying to do this `data.user.username`, we try to read username from user but user is undefined since data is null. This is what the error is telling us: 'Cannot read properties of undefined (reading username)'. Let's fix this.

## Error handling in NextAuth authorize function

In a [previous chapter](https://dev.to/peterlidee/how-to-add-error-handling-to-nextauth-googleprovider-1903) we had to handle errors in the `jwt callback` with the GoogleProvider. There too we had to make a `Strapi` auth request that could return an strapiError. Inside our `jwt callback` we handled this error by throwing an error. We then had a very limited option to handle this in our _front-end_ UI by reading our the error searchParam on the `/signin` or `/authError` pages. We've seen this and I won't repeat it. The point here is that we now need to do something very similar. Again, error handling on a `Strapi` auth request inside a callback function.

As inside our `jwt callback`, we can throw an error to interrupt the auth flow. When throwing an error inside `authorize`, the auth flow will be stopped (no more callbacks will be called and the user won't get signed in) and we get an error to work with in our UI.

But there is a different way to stop the auth flow from inside `authorize`: by returning null. Returning null also stops the auth flow but it will send back one of the default `NextAuth error codes` that we covered in a [previous chapter](https://dev.to/peterlidee/how-to-add-error-handling-to-nextauth-googleprovider-1903). How does it send these back, via the error searchParam that we know from the `jwt callback`.

Let me demonstrate this. We temporarily edit our `authorize` function:

```jsx
async authorize(credentials, req) {
  return null;
}
```

When we sign in using credentials, `authorize` will always return null. This will:

1. Stop the auth flow (the user won't get signed in).
2. Return one of the internal `NextAuth` errors via an error searchParam.

Testing this, confirms this:

![NextAuth authorize return null](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t30j0o2g0njn0clbw4ni.png)

We are not signed in. We are not redirected. Our url now has an error searchParam:

```
http://localhost:3000/signin?error=CredentialsSignin (edited)
```

The value of this error searchParam is one the the [NextAuth error codes](https://next-auth.js.org/configuration/pages#error-codes): CredentialsSignin. On top of this, the error handling we initially setup for GoogleProvider works here too and will display the error below the form.

Quick recap, we can interrupt the auth flow in the `authorize` function either by throwing an error or by returning null. By returning null `Nextauth` will use it's internal error handling that we know from error handling inside the `jwt callback`. This means that returning null inside `authorize` kinda equals throwing an error inside the `jwt callback`.

Throwing an error inside `authorize` works completely different from throwing an error inside `jwt callback`. Let's try this out:

```jsx
async authorize(credentials, req) {
  throw new Error('foobar');
}
```

And we try to sign in:

![NextAuth authorize throw error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/05cyf78mep5wq6obzgc7.png)

We are redirected to `/authError` and have an error searchParam: 'foobar'. Note that this is the same process we got in the beginning of this chapter.

```
http://localhost:3000/authError?error=foobar
```

Here is the cool thing. We can handle this error differently, manually.

## Using NextAuth signIn function to handle errors

We can prevent `NextAuth` from redirecting when we throw an error inside `authorize`. How? By setting an extra parameter `redirect: false` in the `signIn` function:

```jsx
signIn('credentials', {
  identifier: data.identifier,
  password: data.password,
  redirect: false,
});
```

This does not only change the auth flow (by not redirecting), it also changes the behavior of `signIn`. `signIn` will now return a value:

```ts
{
  /**
   * Will be different error codes,
   * depending on the type of error.
   */
  error: string | undefined;
  /**
   * HTTP status code,
   * hints the kind of error that happened.
   */
  status: number;
  /**
   * `true` if the signin was successful
   */
  ok: boolean;
  /**
   * `null` if there was an error,
   * otherwise the url the user
   * should have been redirected to.
   */
  url: string | null;
}
```

Where error will be the error we throw in `authorize`. The means that we now have direct access in our _frontend_ UI to the errors we throw in our `authorize` function. We will have to update `signIn` to await the response and update our handleSubmit to be async:

```jsx
const signInResponse = await signIn('signin', {
  identifier: validatedFields.data.identifier,
  password: validatedFields.data.password,
  redirect: false,
});
console.log('signInResponse', signInResponse);
```

In our current example signInResponse will then look like this:

```js
// this will only log in our browser console
{
  "error": "foobar",
  "status": 401,
  "ok": false,
  "url": null
}
```

## Recap

Let me recap this entire process. We need to handle a possible `Strapi` error inside our `authorize` function. There are 3 ways to stop the `authorize` function/callback:

1. `return null` will trigger the internal `NextAuth` error handling. We will stay on the _sign in_ page and an error searchParam is added to the url.
2. `throw new Error` will triggers another internal `NextAuth` error handling process. We are redirected to `authError` (a custom `NextAuth` error page), again get an error searchParam but this time, the value of said param equals the value of the error we threw inside `authorize`.
3. `throw new Error` + add `redirect: false` option in the `signIn` function will skip the redirect and the error searchParam. Instead, `signIn` will now return an object. We can then use this return value to handle the error inside our form component.

I took a long time to explain this but I do hope this makes sense. It's all a bit confusing but I hope I cleared everything up.

A note before we go on. Can we use this same process for GoogleProvider? No, the redirect option only works for the email and credentials providers.

## Coding time

It's time to actually write our code. We do the `Strapi` error handling inside the `authorize` function first and then we'll look at the form component. This is where we left of:

```jsx
async authorize(credentials, req) {
  const strapiResponse = await fetch(
    `${process.env.STRAPI_BACKEND_URL}/api/auth/local`,
    {
      method: 'POST',
      headers: {
        'Content-type': 'application/json',
      },
      body: JSON.stringify({
        identifier: credentials!.identifier,
        password: credentials!.password,
      }),
    }
  );

  const data: StrapiLoginResponseT = await strapiResponse.json();
  return {
    name: data.user.username,
    email: data.user.email,
    id: data.user.id.toString(),
    strapiUserId: data.user.id,
    blocked: data.user.blocked,
    strapiToken: data.jwt,
  };
},
```

Since we do a fetch, we will wrap it inside a try catch block. Then, we will check if strapiResponse is ok or not and handle the not ok part. (The ok part is already done) Here is our updated function:

```jsx
async authorize(credentials, req) {
  try {
    const strapiResponse = await fetch(
      `${process.env.STRAPI_BACKEND_URL}/api/auth/local`,
      {
        method: 'POST',
        headers: {
          'Content-type': 'application/json',
        },
        body: JSON.stringify({
          identifier: credentials!.identifier,
          password: credentials!.password,
        }),
      }
    );

    if (!strapiResponse.ok) {
      // return error to signIn callback
      const contentType = strapiResponse.headers.get('content-type');
      if (contentType === 'application/json; charset=utf-8') {
        const data: StrapiErrorT = await strapiResponse.json();
        throw new Error(data.error.message);
      } else {
        throw new Error(strapiResponse.statusText);
      }
    }

    // success
    const data: StrapiLoginResponseT = await strapiResponse.json();
    return {
      name: data.user.username,
      email: data.user.email,
      id: data.user.id.toString(),
      strapiUserId: data.user.id,
      blocked: data.user.blocked,
      strapiToken: data.jwt,
    };
  } catch (error) {
    // Catch errors in try but also f.e. connection fails
    throw error;
  }
},
```

This should all make sense. We will also add one more line before the try catch block where we test if there are credentials. We will validate this in our form component before we call `signIn` but this is just an extra precaution:

```jsx
// make sure the are credentials
if (!credentials || !credentials.identifier || !credentials.password) {
  return null;
}
```

Why do we return null here? Because it is very unlikely to happen due to our form validation beforehand and we just let `NextAuth` handle it.

In the next chapter we will handle our form component.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).