Why can't I use a try-catch block inside a server action function?
Answered
Western paper wasp posted this in #help-forum
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.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
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
page.tsx
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?
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 anywayBlack Turnstone
What about this? Is this comparable?
actions.tsx
page.tsx
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
tooWestern 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 wasp Which is weird, because I thought server actions ran on the server? 🤷♂️
Black Turnstone
Yes it does
Western paper waspOP
My NextJS application is running on port 3000, and if I inline this fetch:
into my server action
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
accordinglyWestern 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.
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.
@luis_llanes 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
I'm so sorry. I'm apparently an idiot now.
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.