Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add way to show error in promise toast with server actions #450

Open
Bazyli12 opened this issue Jun 14, 2024 · 2 comments
Open

Comments

@Bazyli12
Copy link

Describe the feature / bug 📝:

it is posible to handle errror in toast.promise in other way than throwing new error [throw new Error("MESSAGE")] in async function?

i have toast with promise function where i am deleting user, if i have error in server function i wanna have error variant of toast, it is possible with throwing error but it also give it in console etc.

function in client component

onClick={async () => {
    toast.promise(
        deleteUser({
            email: row.getValue("email"),
        }),
        {
            loading: "Usuwanie...",
            success: ({ data }) => data,
            error: ({ message }) => message,
            finally: () => {
                // final
            },
        }
    );
}}

server action function

"use server";

import prisma from "@/lib/prisma";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { revalidatePath } from "next/cache";

const deleteUserSchema = z.object({
    email: z.string().email(),
});

export async function deleteUser(values: z.infer<typeof deleteUserSchema>) {
    const schema = deleteUserSchema.safeParse(values);
    const { user, session } = await auth();

    if (!user) {
        throw new Error("Musisz być zalogowany");
    }

    if (user?.email === values.email) {
        // it works but it also throw error in console and i dont want it...
        throw new Error("Nie możesz usunąć swojego konta");
    }

    if (!schema.success) {
        return { success: true, data: schema.data };
    }
    const { email } = schema.data;

    const deletedUser = await prisma.user.delete({
        where: {
            email,
        },
    });

    if (!deletedUser) {
        throw new Error("Taki użytkownik nie istnieje");
    }

    revalidatePath("/(dashboard)/dashboard/members");

    return {
        success: true,
        data: `Użytkownik ${email} został usunięty`,
    };
}

stupid sollution

// client
onClick={async () => {
    setLoading(true);

    const toastTest = toast;

    toastTest.loading("Usuwanie...");

    const { success, data } = await deleteUser({
        email: row.getValue("email"),
    });

    toastTest.dismiss();

    if (success) {
        toastTest.success(data);
    } else {
        toastTest.error(data);
    }
}}

// server
if (user?.email === values.email) {
    return { success: false, data: "Nie możesz usunąć swojego konta" };
}

it works but it pretty goofy

it is the best solution but also very goofy and long...

// client
onClick={async () => {
    async function generatePromise<T>(
        promise: Promise<{ success: boolean; data: T }>
    ): Promise<T> {
        const action = await promise;
        if (action.success) {
            return action.data;
        } else {
            throw action.data;
        }
    }
    
    const promise = generatePromise(deleteUser({ email: row.getValue("email") }));

    toast.promise(promise, {
        loading: "Usuwanie...",
        success: (data) => data,
        error: (message) => message,
        finally: () => {
            // final
        },
    });
}}

// server - same as above
if (user?.email === values.email) {
    return { success: false, data: "Nie możesz usunąć swojego konta" };
}
@Bazyli12
Copy link
Author

ok after thinking about it, the best way is to add another example in docs with server action...
image

but I dont think there is anyone who also uses sonner with server actions in this way xd

@Rwaja
Copy link

Rwaja commented Sep 9, 2024

In my case I didn't care that the error was displayed in the server console, my problem was that the error message was displayed correctly in development but not in production

"use server" turns all functions in your actions.ts file into endpoints for the client.
That is why handling errors with the Error instance does not work correctly in production, because Next uses a structure defined for these cases and avoid leaks of important information in the response.
https://nextjs.org/docs/app/api-reference/file-conventions/error#error

Therefore you must find a way to "reject" the promise with an error without using Error instance in the server action.

Works in development:

//actions.ts
'user server'
export async function serverAction(){
    const { error, someResult } = await someDatabaseQuery();
    if (error) {
        throw new Error("Error:" error);
    }
    if (!someResult) {
        throw new Error("Error message");
    }
    return someResult
}
//page.tsx
'use client'
toast.promise(serverAction()), {
      loading: 'await response...',
      success: 'response ok!',
      error: (error) => error.message,
});

but in production the error message is not shown, instead it is shown:
An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.

Solution

This works for me, it just reduces generatePromise function a little, but it's still the same:

//utils.ts
export async function customPromise<T>(promise: Promise<{ success: boolean; data: T }>): Promise<T> {
  const { success, data } = await promise;
  if (success) {
    return data;
  }
  throw data;
}

avoid generating an Error instance in the server action:

'user server'
export async function serverAction(){
    const { error, someResult } = await someDatabaseQuery();
    if (error) {
        return { success: false, data: error.message || "Error message" };
    }
    if (!someResult) {
        return { success: false, data: "Error message" };
    }
    return { success: true, data: someResult };
}

on the client I use customPromise. This function generates a promise that may fail and return the error message:

 toast.promise(customPromise(serverAction()), {
      loading: 'await response...',
      success: 'response ok!', //you can get someResult
      error: (message) => message,
});

Personally I would like them to include this in the documentation, but I don't think there are many use cases, since in next js server actions are not the only way to do fetching and it is not the most preferred by everyone. So this will probably never be added...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants