Quick check, if you have any Server Action handling a checkbox, a "subscribe to newsletter," an "I agree to terms," a "keep me signed in," pull it up right now and look closely at how you're reading that field's value. There's a very common, very quiet bug hiding in exactly this spot.
The Setup That Looks Completely Reasonable
<form action={submitContact}>
<input name="name" />
<input name="email" type="email" />
<input type="checkbox" name="subscribe" />
<button type="submit">Submit</button>
</form>
'use server';
export async function submitContact(formData: FormData) {
const subscribe = Boolean(formData.get('subscribe'));
// ...
await saveContact({ name, email, subscribe });
}
This looks correct. It reads cleanly. It also has a real bug, and it's the kind that only shows up once you specifically go check the actual saved data, not by looking at the code.
Why This Actually Breaks
FormData.get() returns either the string value of a field, or null if the field wasn't present at all. A checked checkbox sends the string "on" by default (or whatever value you explicitly set). An unchecked checkbox sends nothing at all, meaning formData.get('subscribe') returns null in that case.
So far, Boolean(null) correctly evaluates to false, and Boolean("on") correctly evaluates to true. This specific example actually works. The bug shows up the moment someone "improves" it slightly, which happens constantly in real codebases:
// A seemingly reasonable refactor that quietly breaks everything
const subscribe = Boolean(formData.get('subscribe') || 'false');
Boolean('false') is true. The string "false" is a non-empty string, and every non-empty string is truthy in JavaScript, regardless of what word it spells out. This exact pattern, someone adding a fallback value "to be safe" without registering that the fallback itself is now the thing being evaluated for truthiness, is one of the most common ways this bug actually gets introduced into a real codebase.
An Even More Common Version of the Same Root Problem
// A "select" or radio input, values coming through as strings
const isActive = formData.get('isActive'); // "true" or "false", as strings
await User.updateOne({ _id }, { isActive }); // saved as the STRING "true" or "false"
// Later, checking this "boolean" the normal way
if (user.isActive) {
// This is true even when isActive is the string "false"
}
Every value coming out of FormData.get() is a string, or null. Never an actual boolean, never an actual number, regardless of what HTML input type produced it. Storing that raw string directly into a database field meant to represent a real boolean is how a value like isActive: "false" ends up evaluating as truthy every single time it's checked afterward, since it's a non-empty string, not the boolean false.
The Actual Fix
Explicit, deliberate conversion, checking for the exact expected value rather than relying on JavaScript's general truthiness rules to happen to line up correctly:
// ✅ Explicit check against the actual expected string value
const subscribe = formData.get('subscribe') === 'on';
// ✅ Or, using Zod to make the expected shape explicit and validated
import { z } from 'zod';
const ContactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
subscribe: z.coerce.boolean().optional().default(false),
});
// Note: z.coerce.boolean() has its own gotcha, worth checking directly.
// It coerces ANY non-empty string to true, including "false", so
// coercion alone doesn't fix this without an explicit transform matching
// your actual checkbox value convention.
The safest version explicitly checks for the exact string your checkbox actually sends, 'on' by default, or whatever value you set on the input yourself, rather than trusting any generic boolean coercion to happen to handle it the way you'd expect.
Why This Specific Bug Is So Easy to Ship
It's completely silent. No error, no warning, no failed test, if your tests don't specifically check the false case with a real submitted form. The checked case works. Most manual testing during development involves checking the box, since that's usually the "happy path" someone tests first, and the actual bug only shows up for users who deliberately leave a checkbox unchecked, exactly the users least likely to complain, since from their perspective, they got the behavior they wanted, they just don't know your database quietly recorded the opposite.
The Actual Checklist
Never rely on Boolean() alone to interpret a value pulled from FormData. Every value is a string or null, and generic JavaScript truthiness rules do not map cleanly onto "was this checkbox checked."
Check explicitly against the actual value your input sends, === 'on', or whatever you've explicitly set, rather than a generic conversion.
If storing a boolean-like value in a database, confirm it's actually stored as a real boolean, not a string that happens to spell out "true" or "false." A quick check of the actual saved document, not just the code, catches this.
Test the unchecked case specifically, not just the checked one. This is the entire bug, in one sentence, a code path that only gets exercised when someone deliberately doesn't do the obvious thing.
Go check any checkbox-handling Server Action in your own code right now, specifically test submitting the form with it left unchecked, and check what actually got saved. If you find this, you're genuinely not alone, drop it in the comments, curious how common this turns out to be once people actually go look.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)