Next.js Discord

Discord Forum

Why can't I use a try-catch block inside a server action function?

Answered
Western paper wasp posted this in #help-forum
Open in Discord
Western paper waspOP
I am not throwing anything from the server action function itself. I am just catching stuff and then mapping it to serializable objects. But it refuses to work.
Answered by Western paper wasp
While I worked on this, somehow the 'use server'; directive in actions.ts got dropped.
View full answer

68 Replies

Western paper waspOP
Is there some mechanism in NextJS 15 that intercepts all thrown objects regardless of where they are thrown from? (That seems very unlikely to me)
American black bear
You can use try catch in a server action
it's the usual protocole to building a good API

"use server"

export async function createUser(username: string) {
  try {
    if (!username) throw new Error("No username")
    
    // might throw an error
    const newUser = await db.users.insert({username})

    return {
      ok: true,
      data: newUser
    }
  } catch (error) {
    return {
      ok: false,
      error: error
    }
  }
}
Can you show us what are you trying to achieve?

Try catch work in Server Actions, just be careful not to use next.js “redirect” function inside the try block because that function works by throwing an error so you’ll catch it instead of redirecting… apart from that TryCatch should work properly
Western paper waspOP
What xxxxx wrote is exactly the kind of thing I am trying to achieve -- just normal async/await code flow. I will try again, and then report back.
@luis_llanes Have you tried yeT?
Western paper waspOP
It does not work
If I try catching anything in the server action, this is what the client gets: { "environmentName": "Server", "digest": "2950434531" }
No matter what I throw
Western paper waspOP
Basically anything that is caught in the catch-block has this shape, regardless of what was actually thrown

{
  "environmentName": "Server",
  "digest": "3298651043"
}
🤷
Could you show the way you're doing it? or replicate it so we can have the full context?
Maybe share a little repo or code snippet
Western paper waspOP
Before I do that, is there anywhere I can read about the server-action execution model in NextJS? I mean, the official docs are just surface-level stuff (maybe I need to read the source code for that?)
I mean you don't have to show the real implementation just one that follows the same approach you are using
This behaviour must be Next.js preventing the error to leak to the client by transforming them into this format
Black Turnstone
Hi, I also tried implementing a basic server action with try catch and it seems to work just fine for me. Here is my implementation:

actions.ts
'use server';

export const createUser = async (username: string) => {
    try {
        if (!username) throw new Error('Username is required');

        return {
            success: true,
            message: 'User created successfully',
            user: {
                username,
            },
        };
    } catch (error) {
        return {
            success: false,
            message: 'User creation failed',
            error:
                error instanceof Error ? error.message : 'An unknown error occurred',
        };
    }
};


page.tsx
'use client';
import React, { useState } from 'react';
import { createUser } from './actions';

const HomePage = () => {
    const [username, setUsername] = useState('');

    const handleClick = async () => {
        const result = await createUser(username);
        console.log(result);
    };

    return (
        <div className="flex h-dvh w-dvh items-center justify-center flex-col gap-2 p-4">
            <input
                type="text"
                name="username"
                placeholder="Username"
                className="text-black"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
            />
            <button onClick={handleClick}>Create User</button>
        </div>
    );
};

export default HomePage;
You have something like this? (super simple)

"use server"

export async function example(number: number) {
  try {
    if(number === 1){
      throw new Error("No username")
    }

    return {
      success: true,
      data: number
    }
  } catch (error) {
    return {
      success: false,
      error: error
    }
  }
}
Western paper waspOP
I think the difference in our cases @Black Turnstone is that I try to throw an object, and then relay that information in the returned object in the server action.
Maybe it's different when throwing new Error(...) vs throwing obj
So here is my example:
Black Turnstone
@Western paper wasp
Something like this?
'use server';

export const createUser = async (username: string) => {
    try {
        if (!username)
            throw {
                message: 'Username is required',
                code: 400,
            };

        return {
            success: true,
            message: 'User created successfully',
            user: {
                username,
            },
        };
    } catch (error) {
        return {
            success: false,
            message: 'User creation failed',
            error: error,
        };
    }
};
Western paper waspOP
const handleResponse = async (response: Response) => {
    const json = await response.json();

    if (!response.ok) {
        throw json;
    }

    return json;
}

export async function postFileToProject(projectGuid: string, file: FormDataEntryValue) {
    const headers = {
        'Authorization': `Bearer ......................`,
    }

    const formData = new FormData();
    formData.append("file", file);

    const response = await fetch(`${host}/api/v1/project/${projectGuid}/upload`, {
        method: 'POST',
        headers,
        body: formData
    });

    return await handleResponse(response)
}
where's the try catch?
Western paper waspOP
Just a sec. This is the "api service" 👆
Which is then used by the server action, here:
export async function postFileToProject(previousState, formData: FormData) {

    const projectGuid = formData.get("projectGuid") as string;
    const file = formData.get("file") as FormDataEntryValue;

    try {
        await another_api.postFileToProject(projectGuid, file)
    } catch (json) {
        return json;
    }
}
I named the catch-variable json, to match the fact that I throw a json object in another_api
If I abstain from throwing anything, this works perfectly
Western paper waspOP
Ideally I would like to subclass Error with additional fields. But of course, that doesn't work either. Because I can't catch any useful stuff in the server action anyway
Black Turnstone
What about this? Is this comparable?

actions.tsx
'use server';
export async function postFileToProject(_prev: unknown, formData: FormData) {
    const projectGuid = formData.get('projectGuid') as string;
    const file = formData.get('file') as FormDataEntryValue;

    try {
        console.log(projectGuid, file);
        if (!projectGuid) {
            throw {
                message: 'Project GUID and file are required',
            };
        }
        return {
            data: projectGuid,
            message: 'File uploaded successfully',
        };
        // await another_api.postFileToProject(projectGuid, file);
    } catch (json) {
        return json;
    }
}


page.tsx
'use client';

import { useActionState } from 'react';
import { postFileToProject } from './actions';

const HomePage = () => {
    const [message, submitAction, isPending] = useActionState(
        postFileToProject,
        null
    );

    console.log('🚀 ~ HomePage ~ message:', message);

    return (
        <form
            action={submitAction}
            className="flex h-dvh w-dvh items-center justify-center flex-col gap-2 p-4"
        >
            <input
                type="text"
                name="projectGuid"
                placeholder="Project GUID"
                className="text-black"
            />
            <input type="file" name="file" placeholder="File" />
            <button type="submit" disabled={isPending}>
                {isPending ? 'Uploading...' : 'Upload'}
            </button>
        </form>
    );
};

export default HomePage;
Western paper waspOP
Let my quickly run that
I use useActionState too
Western paper waspOP
Ok. Say my server action is this:

export async function postFileToProject(previousState, formData: FormData) {
    try {
        throw { hello: 'blabla' }
    } catch (e) {
        return e;
    }
}
Then that state actually becomes the message, here

    const [message, submitAction, isPending] = useActionState(
        postFileToProject,
        null
    );
So let me try inlining the whole thing from another_api
See what happens then
Western paper waspOP
When inlining, it becomes another case completely
Then I get CORS issues
Black Turnstone
Can you share code for another_api
Western paper waspOP
Which is weird, because I thought server actions ran on the server? 🤷‍♂️
Western paper waspOP
My NextJS application is running on port 3000, and if I inline this fetch:

const response = await fetch(`http://localhost:8080/api/v1/project/${prosjectGuid}/upload`, {
        method: 'POST',
        headers,
        body: formDataInner
    });


into my server action
Then suddenly I see this as a POST directly from the browser:
Wtf is going on lol
(don't worry about the project vs. prosjekt difference, I translated the name as I went along)
Are you sure the external api running on :8080 has CORS properly set up?
Western paper waspOP
I can disable CORS on it. But there is no reason whatsoever for the browser to try to POST directly to localhost:8080
That is why I used a server action in the first place
Black Turnstone
Try this (actions.ts) also make sure to add "use server" at top of action file.
'use server';
export async function postFileToProject(_prev: unknown, formData: FormData) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const projectGuid = formData.get('projectGuid') as string;
    const file = formData.get('file') as FormDataEntryValue;
    const headers = {
        'Content-Type': 'multipart/form-data',
    };
    const formDataInner = new FormData();
    formDataInner.append('file', file);
    formDataInner.append('projectGuid', projectGuid);
    try {
        const response = await fetch(
            `http://localhost:8080/api/v1/project/${projectGuid}/upload`,
            {
                method: 'POST',
                headers,
                body: formDataInner,
            }
        );
        return response;
    } catch (json) {
        return json;
    }
}
just update headers and formDataInner accordingly
Western paper waspOP
Ah, I might have removed the 'use server'; as I refactored. Doh.. That explains the browser calling it.
Black Turnstone
Yes
Western paper waspOP
My mistake
@Western paper wasp My mistake
Black Turnstone
Works now?
Original message was deleted
Black Turnstone
I don't think you need to inline this. It should have worked with abstracted function too.
Just check once there might be some mistake in that abstracted function.
Western paper waspOP
While I worked on this, somehow the 'use server'; directive in actions.ts got dropped.
Answer
Western paper waspOP
And that seems to explain everything at this point
I guess I added it in the working copy, and after reverting, I undo-ed that part
I guess it goes to show how easy it is to fuck up when using server actions.
@Western paper wasp While I worked on this, somehow the `'use server';` directive in `actions.ts` got dropped.
omg noooo it was actually the first thing I thought about but how could that be if the title says "server action" lol Why does that happen all the time? 😭
Western paper waspOP
Thanks @Black Turnstone and @luis_llanes for your patience. And sorry for kind of wasting your time. Ideally there should be some mechanism in which NextJS warns the developer when this happens.
Don't worry, no time wasted next time the "use server" directive will be the first thing we check I'm sure lol
Western paper waspOP
I'll check out the help-forum going forward, making sure to point this out for anyone else. To redeem myself
Btw @Western paper wasp mark the solution for rest of the people.