export async function action() { throw new Error('Failed to save data'); } export default function Page() { async function handleSubmit() { await action(); } return <button onClick={handleSubmit}>Submit</button>; }
If a server action throws an error and it is not caught, Next.js will respond with a 500 error. The client receives the error as a rejected promise but since it is not handled, the UI will fail silently.
Option A uses the correct try-catch syntax and accesses the error message properly. Option A has a misplaced catch block. Option A uses finally incorrectly to return an error. Option A misses the error parameter in catch.
import { useState } from 'react'; export async function action() { throw new Error('Save failed'); } export default function Page() { const [error, setError] = useState(null); async function handleClick() { try { await action(); } catch (e) { setError(e.message); } } return ( <> <button onClick={handleClick}>Save</button> {error && <p role="alert">Error: {error}</p>} </> ); }
The error is caught and stored in state. React re-renders showing the error message below the button. The button remains visible.
export async function action() { throw new Error('Oops'); } export default function Page() { async function handleSubmit() { await action(); } return <button onClick={handleSubmit}>Submit</button>; }
If the client code calling the server action does not catch errors, they do not propagate to the UI. The error is lost silently unless caught and handled.
Best practice is to catch errors inside server actions and return a clear error object. The client can then check this and update the UI accordingly. Relying on automatic error display or ignoring errors leads to poor UX.