T
TanStack9mo ago
sunny-green

Returning custom errors from createServerFn

In Next.js, I can't control anything about the response of a server action. It's either 200 OK with response data, or it's 500 where next assumes an unrecoverable error happened, when in reality I just want to return for example a 404 with the message "Could not find a user with that phone number". This means I have to resort to an ungly hack where I wrap my own ok: true or ok: false just to be able to pass any errors back to the client. This is what my nextjs solution is
// type LoginActionResponse =
// | { ok: true; data: Pick<LoginResponse, "username" | "image" | "verified"> }
// | {
// ok: false;
// code:
// | "InvalidCredential"
// | "InvalidLogin"
// | "Unknown"
// | "MissingTOTP"
// | "WrongTOTP";
// };

// Create a custom refinement for validating a phone number
const phoneNumberSchema = z
.string()
.refine((value) => true, { message: "Invalid phone number" });

// Create a schema for validating an email
const emailSchema = z.string().email({ message: "Invalid email address" });

// Combine the phone and email schemas into one using z.union
const credentialSchema = z.union([phoneNumberSchema, emailSchema]);

// Create the full login schema
const loginSchema = z.object({
credential: credentialSchema,
password: z.string(),
isrememberme: z.boolean(),
});

export const login = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
const res = await fetch(
`${baseUrl}login`,
postJson({
credential: data.credential,
password: data.password,
}),
);

try {
const userData = (await handleJsonWithCodeIfOk(
res,
"login",
)) as LoginResponse;
await setAuthCookies(userData);
return {
ok: true,
data: {
username: userData.username,
image: userData.image,
verified: userData.verified,
},
};
} catch (e) {
if (e instanceof Error) {
return {
ok: false,
code: e.message as any,
};
}
return {
ok: false,
code: "Unknown",
};
}
});
// type LoginActionResponse =
// | { ok: true; data: Pick<LoginResponse, "username" | "image" | "verified"> }
// | {
// ok: false;
// code:
// | "InvalidCredential"
// | "InvalidLogin"
// | "Unknown"
// | "MissingTOTP"
// | "WrongTOTP";
// };

// Create a custom refinement for validating a phone number
const phoneNumberSchema = z
.string()
.refine((value) => true, { message: "Invalid phone number" });

// Create a schema for validating an email
const emailSchema = z.string().email({ message: "Invalid email address" });

// Combine the phone and email schemas into one using z.union
const credentialSchema = z.union([phoneNumberSchema, emailSchema]);

// Create the full login schema
const loginSchema = z.object({
credential: credentialSchema,
password: z.string(),
isrememberme: z.boolean(),
});

export const login = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
const res = await fetch(
`${baseUrl}login`,
postJson({
credential: data.credential,
password: data.password,
}),
);

try {
const userData = (await handleJsonWithCodeIfOk(
res,
"login",
)) as LoginResponse;
await setAuthCookies(userData);
return {
ok: true,
data: {
username: userData.username,
image: userData.image,
verified: userData.verified,
},
};
} catch (e) {
if (e instanceof Error) {
return {
ok: false,
code: e.message as any,
};
}
return {
ok: false,
code: "Unknown",
};
}
});
And this is what I ideally want
export const login = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
const res = await fetch(
`${baseUrl}login`,
postJson({
credential: data.credential,
password: data.password,
}),
);

if (!res.ok) {
const err = await res.json();
// Not sure which vinxi method is best to use
sendError(err);
}

const userData = (await res.json()) as LoginResponse;
// My own function that just wraps setCookie from vinxi/http
setAuthCookies(userData);
return userData;
});
export const login = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
const res = await fetch(
`${baseUrl}login`,
postJson({
credential: data.credential,
password: data.password,
}),
);

if (!res.ok) {
const err = await res.json();
// Not sure which vinxi method is best to use
sendError(err);
}

const userData = (await res.json()) as LoginResponse;
// My own function that just wraps setCookie from vinxi/http
setAuthCookies(userData);
return userData;
});
How are you all returning errors from your server function?
4 Replies
sunny-green
sunny-greenOP9mo ago
If I try to throw createError() like this:
export const login = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
throw createError({
status: 422,
message: "Unknown error",
statusMessage: "status message",
statusText: "status text",
cause: "causesss",
data: { data: "data" },
name: "name of error",
});
})
export const login = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
throw createError({
status: 422,
message: "Unknown error",
statusMessage: "status message",
statusText: "status text",
cause: "causesss",
data: { data: "data" },
name: "name of error",
});
})
I just get
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
adverse-sapphire
adverse-sapphire9mo ago
you can throw json throw json(body, { status: 400 })
correct-apricot
correct-apricot9mo ago
we have a PR coming up that should fix this
sunny-green
sunny-greenOP9mo ago
Oh wow, that's sexy

Did you find this page helpful?