JAVASCRIPT / CONDITIONALS
Validating input before acting on it
Check untrusted values for presence, type, then range in that order, and coerce once, so bad input is refused with a reason instead of silently converted.
What you will learn
- Reject empty and whitespace input before coercing: Number("") and Number(null) are 0
- Use Number.isFinite or Number.isInteger to reject NaN, Infinity and strings at once
- Test presence with x == null instead of !x, which also rejects 0, "" and false
- Return { ok, value, reason } so the caller gets the cleaned value and the failure cause
Understanding Validating input before acting on it
JavaScript almost never refuses a value. Number("") is 0, Number(null) is 0, "3" * 2 is 6 while "3" + 2 is "32", and every comparison against NaN is false. Bad input therefore does not stop your function; it slides through and produces a believable wrong answer that ends up in a total, a stored record, or a rendered page. Validation is the point where you refuse a value while you still know where it came from and what it was supposed to mean.
Treat a validator as a funnel that shrinks the set of values allowed to reach the code below it, in a fixed order: is it present, is it the right type, normalize it once, then is it in range. The order is not cosmetic. raw.trim() throws if raw is null, and n < 1 cannot reject NaN because relational comparisons with NaN are always false, so a range check placed before a type check quietly approves garbage. Coerce exactly once, right where the type is confirmed, and keep that result; re-coercing later is how a value gets checked as a number and then used as a string.
Validate where untrusted data enters: a form field, a JSON.parse result, a URL query string, arguments handed to you by code you do not control. Convert it there into a trusted shape so the functions underneath do not each repeat the same defensive tests, and stay aware of what the entry point can really hand you, since JSON.parse("null") succeeds and typeof null is "object", so a shape check needs value !== null too. Report a reason rather than just true or false, because the caller nearly always has to show a message or pick a fallback, and packaging the reason together with the cleaned value means nobody parses the same input twice.
A validator is only useful if the code after it is allowed to relax. Once parseQuantity has returned a confirmed integer, downstream functions should not re-test it; duplicated checks drift apart over time and the weakest copy becomes the real rule.
function parseQuantity(raw) {
if (typeof raw !== "string") return { ok: false, reason: "must be text" };
const trimmed = raw.trim();
if (trimmed === "") return { ok: false, reason: "required" };
const n = Number(trimmed);
if (!Number.isInteger(n)) return { ok: false, reason: "must be a whole number" };
if (n < 1 || n > 99) return { ok: false, reason: "must be 1 to 99" };
return { ok: true, value: n };
}
const inputs = ["7", " 12 ", "", "3.5", "0", "12abc", "1e3", null];
for (const raw of inputs) {
const result = parseQuantity(raw);
const shown = result.ok ? "value " + result.value : "rejected: " + result.reason;
console.log(JSON.stringify(raw), "->", shown);
}Order your checks so each one can trust the one before it, because a skipped check in JavaScript produces a plausible wrong value instead of an error.
Worked examples
Collecting every failing field
Validates a whole object in one pass so a form can report all of its problems at once.
function validateSignup(form) {
const errors = [];
if (typeof form.email !== "string" || !form.email.includes("@")) errors.push("email");
if (!Number.isFinite(form.age) || form.age < 13) errors.push("age");
if (form.terms !== true) errors.push("terms");
return errors;
}
const forms = [
{ email: "ada@example.com", age: 30, terms: true },
{ email: "ada@example.com", age: NaN, terms: true },
{ email: "", age: 12, terms: "yes" },
{ email: "ada@example.com", age: "30", terms: true }
];
for (const form of forms) {
const errors = validateSignup(form);
console.log(errors.length === 0 ? "accepted" : "rejected: " + errors.join(", "));
}Example explained
Line 1typeof form.email !== "string" comes first so .includes is only ever called on a real string; with the tests swapped, a missing email would throw a TypeError.
Line 2Number.isFinite(form.age) is false for NaN, for undefined, and for the string "30", so one call covers missing, unparsed and wrong-typed ages.
Line 3form.terms !== true demands the exact boolean, so "yes", 1 and a missing key all fail, where a falsy test would have accepted "yes".
Line 4Pushing names into errors instead of returning at the first problem lets the third form report three fields in a single submit.
Missing is not the same as falsy
Shows how a presence check written as !value rejects legitimate zero, and what to write instead.
function describeRating(rating) {
if (!rating) return "no rating given";
return "rating: " + rating;
}
function describeRatingFixed(rating) {
if (rating == null) return "no rating given";
if (!Number.isInteger(rating) || rating < 0 || rating > 5) return "invalid rating";
return "rating: " + rating;
}
console.log(describeRating(4));
console.log(describeRating(0));
console.log(describeRatingFixed(0));
console.log(describeRatingFixed(undefined));
console.log(describeRatingFixed("4"));Example explained
Line 1!rating is true for 0, "" and false, so the first function reports a real rating of 0 as absent.
Line 2rating == null is true for exactly null and undefined, which is the one place loose equality is the clearest tool available.
Line 3Number.isInteger("4") is false, so the string form is refused instead of being concatenated into "rating: 4", which would have looked identical in the output.
Line 4The two value checks sit after the presence check, so rating < 0 is only ever compared against something that is already a number.
Normalizing before comparing
Demonstrates that a set-membership check has to run against a cleaned value, not the raw one.
const SIZES = ["small", "medium", "large"];
function parseSize(raw) {
if (typeof raw !== "string") return null;
const key = raw.trim().toLowerCase();
return SIZES.includes(key) ? key : null;
}
console.log(parseSize("Large"));
console.log(parseSize(" medium "));
console.log(parseSize("LARGE"));
console.log(parseSize("huge"));
console.log(parseSize(undefined));Example explained
Line 1trim().toLowerCase() runs only after the typeof test, because both methods throw on undefined.
Line 2SIZES.includes(key) compares the normalized value, so "Large" and " medium " are accepted without adding variants to the list.
Line 3Returning null for anything unrecognized means the caller has one thing to test rather than guessing which spellings got through.
Important notes
Global isFinite and isNaN coerce their argument first, so isFinite("30") is true; Number.isFinite and Number.isNaN do not, which is what a validator wants.
Checks in the browser are feedback for the user, not protection; whatever the page validates has to be validated again wherever the data is received and stored.
Common mistakes
Coercing before testing for empty: Number(box.value.trim()) turns an untouched input into 0, so a quantity or price of 0 is stored as though the user typed it.
Using if (!value) as a presence test: a rating of 0, a comment of "", and an unchecked box holding false are all reported as missing.
Putting the range check first: NaN < 13 is false, so if (age < 13) reject; lets Number("abc") straight through and NaN lands in your data as an age.
Try it yourself
Change, predict, then run
In a browser editor, write parsePercent(raw) that accepts only a string holding a whole number from 0 to 100 and returns { ok: true, value } or { ok: false, reason }. Run it on "0", " 50 ", "", "101", "5%" and null, and confirm "0" is accepted while "" is refused as empty instead of becoming 0.
Open the JavaScript workspaceCheck your understanding
function setAge(raw) { const age = Number(raw); if (age < 18) return "too young"; return "accepted: " + age; } What does setAge("abc") return?
- "accepted: NaN", because NaN < 18 is false so the range check cannot reject a non-number
- "too young", because Number("abc") is not a usable age
- "accepted: 0", because Number("abc") coerces to 0
- Nothing is returned; Number throws a TypeError on a non-numeric string
Show answer
Number("abc") is NaN, and every relational comparison involving NaN evaluates to false, so age < 18 is false and the junk value is reported as accepted; only a type check such as Number.isFinite(age) can catch it. "too young" is tempting because refusing nonsense feels like the safe default, but the code only refuses values provably below 18, and NaN is not comparable at all. Number("") is 0; Number("abc") is not.