Best Practices for Handling Errors in a query-Wrapped Server Function

Hey everyone! I’ve been working with SolidStart and have a question about error handling and display strategies for server functions wrapped in a query. The example below is inspired by one of the official SolidStart examples:
export const getUser = query(async () => {
"use server";
try {
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw new Error("User not found");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw new Error("User not found");
return { id: user.id, username: user.username };
} catch {
await logoutSession();
throw redirect("/login");
}
}, "user");
export const getUser = query(async () => {
"use server";
try {
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw new Error("User not found");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw new Error("User not found");
return { id: user.id, username: user.username };
} catch {
await logoutSession();
throw redirect("/login");
}
}, "user");
In this example, if the user isn’t found, the current approach redirects to the login page. However, for a different scenario, I’d like to display an error message in the UI instead of redirecting. I’m considering a couple of approaches: 1. Relying on Error Boundaries to catch and render the error. 2. Returning a value (e.g., { data: ..., error: ... }) that includes potential errors so I can display the appropriate state in the receiving component. What do you think is the best way to handle this in SolidStart? I’d love to hear your thoughts and how you approach similar cases in your projects. Thanks in advance.
6 Replies
peerreynders
peerreynders4d ago
TL;DR: Return Errors; throw redirects I assume this comes from the with-auth example. To be honest that version doesn't make a lot of sense to me as the "User not found " Error would simply get swallowed in favour of the thrown redirect. Instead have a look at how the strello app handles it with a more distributed approach:
export const getUser = query(async () => {
"use server";
const userId = await getAuthUser();
if (!userId) throw redirect("/login");
const user = await db.account.findUnique({ where: { id: userId } });
if (!user) throw redirect("/login");
return { id: user.id, email: user.email };
}, "user");
export const getUser = query(async () => {
"use server";
const userId = await getAuthUser();
if (!userId) throw redirect("/login");
const user = await db.account.findUnique({ where: { id: userId } });
if (!user) throw redirect("/login");
return { id: user.id, email: user.email };
}, "user");
Ryan Carniato
YouTube
SolidStart: The Shape of Frameworks to Come
Join me to take a look at what SolidStart is shaping up to be. I will be going through a full tour of the framework to show you what it is, what you can do with it, and how it changes things. [0:00:00] Intro [0:05:30] The Shape of Frameworks to Come [0:18:00] A Classic Client-Side Solid App [0:30:45] Simple Todo App with a Router & a Form [0:45...
GitHub
strello/src/lib/index.ts at 9c9ae973d96cc045914e696757a1b5f31efc6fa...
Contribute to solidjs-community/strello development by creating an account on GitHub.
GitHub
solid-start/examples/with-auth/src/lib/index.ts at 5812ac69632d0d3f...
SolidStart, the Solid app framework. Contribute to solidjs/solid-start development by creating an account on GitHub.
peerreynders
peerreynders4d ago
There are only two outcomes, either: - resolve to the user info - reject with a redirect compelling the client side router to navigate to /login (or simply stay there) getUser() is used on the lop level Layout:
const user = createAsync(() => getUser());
const user = createAsync(() => getUser());
This accomplishes two things:
<Show when={user()} fallback={<A href="/login">Login</A>}>
<form action={logout} method="post">
<button name="logout" type="submit">
Logout
</button>
</form>
</Show>
<Show when={user()} fallback={<A href="/login">Login</A>}>
<form action={logout} method="post">
<button name="logout" type="submit">
Logout
</button>
</form>
</Show>
- it makes the user accessor available to show the correct "Login" vs. "Logout" - but more importantly it always throws a redirect('/login') whenever there isn't an active account ID in the session cookie. So you either stay on the /login page or you will get navigated to the /login page.
GitHub
strello/src/components/Layout.tsx at 9c9ae973d96cc045914e696757a1b5...
Contribute to solidjs-community/strello development by creating an account on GitHub.
peerreynders
peerreynders4d ago
Now there is a counterpart:
export const redirectIfLoggedIn = query(async () => {
"use server";

let userId = await getAuthUser();
if (userId) {
throw redirect("/");
}
return null;
}, "loggedIn");
export const redirectIfLoggedIn = query(async () => {
"use server";

let userId = await getAuthUser();
if (userId) {
throw redirect("/");
}
return null;
}, "loggedIn");
This is part of the login route preload:
export const route = {
preload: () => redirectIfLoggedIn(),
} satisfies RouteDefinition;
export const route = {
preload: () => redirectIfLoggedIn(),
} satisfies RouteDefinition;
This ensures that you are automatically navigated from /login to / if there is an authenticated session.
GitHub
strello/src/lib/index.ts at 9c9ae973d96cc045914e696757a1b5f31efc6fa...
Contribute to solidjs-community/strello development by creating an account on GitHub.
GitHub
strello/src/routes/login.tsx at 9c9ae973d96cc045914e696757a1b5f31ef...
Contribute to solidjs-community/strello development by creating an account on GitHub.
peerreynders
peerreynders4d ago
Finally the login.tsx route uses loginOrRegister:
export const loginOrRegister = action(async (formData: FormData) => {
"use server";
const email = String(formData.get("email"));
const password = String(formData.get("password"));
const loginType = String(formData.get("loginType"));
let error = validateEmail(email) || validatePassword(password);
if (error) return new Error(error);

try {
const user = await (loginType !== "login"
? register(email, password)
: login(email, password));
await setAuthOnResponse(user.id);
} catch (err) {
return err as Error;
}
throw redirect("/");
});
export const loginOrRegister = action(async (formData: FormData) => {
"use server";
const email = String(formData.get("email"));
const password = String(formData.get("password"));
const loginType = String(formData.get("loginType"));
let error = validateEmail(email) || validatePassword(password);
if (error) return new Error(error);

try {
const user = await (loginType !== "login"
? register(email, password)
: login(email, password));
await setAuthOnResponse(user.id);
} catch (err) {
return err as Error;
}
throw redirect("/");
});
Again there are only two outcomes: - resolve to an Error instance - reject with a redirect compelling the client side router to navigate to /
peerreynders
peerreynders4d ago
The important feature is that the Error is returned, not thrown. This means that in the login.tsx route we can use the latest submission;
const loggingIn = useSubmission(loginOrRegister);
const loggingIn = useSubmission(loginOrRegister);
to obtain the most recent error from the submission record to display the error:
<Show when={loggingIn.result}>
{(result) => (
<p
class="text-red-500 text-center"
role="alert"
id="error-message"
>
{result().message}
</p>
)}
</Show>
<Show when={loggingIn.result}>
{(result) => (
<p
class="text-red-500 text-center"
role="alert"
id="error-message"
>
{result().message}
</p>
)}
</Show>
So either the login succeeds and you are navigated to / or the Error message is simply displayed in the current /login page.
GitHub
strello/src/routes/login.tsx at 9c9ae973d96cc045914e696757a1b5f31ef...
Contribute to solidjs-community/strello development by creating an account on GitHub.

Did you find this page helpful?