43.4 Parsing and Safeparsing: Throwing vs Returning Results
Right, let’s get into the meat of it. You’ve defined your schema with Zod, and now you want to use it on some sketchy, untrusted data—probably from an API response, a form, or your uncle’s hand-rolled CSV file. You’ve got two main weapons in your arsenal for this: .parse() and .safeParse(). The choice between them isn’t just stylistic; it’s about control flow and how you want to handle the inevitable fact that the universe will send you bad data.
.parse() is the assertive one. It looks at your data, and if it matches the schema, great! You get a beautifully typed value. If it doesn’t? It throws a ZodError right in your face. This is fantastic for situations where a validation failure is a true, show-stopping exception. Think of it like a bouncer at an exclusive club. No ID, no entry, no excuses.
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
const goodData = { name: "Alice", age: 30 };
const user1 = UserSchema.parse(goodData);
// user1 is now of type { name: string; age: number; }
console.log(`Hello, ${user1.name}!`);
const badData = { name: "Bob", age: "thirty" }; // age is a string!
try {
const user2 = UserSchema.parse(badData); // This line throws!
} catch (err) {
if (err instanceof z.ZodError) {
console.error("Validation failed:", err.errors);
// Outputs a helpful array of issues, e.g.:
// [{
// "code": "invalid_type",
// "expected": "number",
// "received": "string",
// "path": ["age"],
// "message": "Expected number, received string"
// }]
}
}
The beauty of the thrown ZodError is its errors property. It’s a collection of all the things that went wrong, not just the first one. This is a lifesaver for debugging, as it tells you every field that’s messed up, not just the one that failed first.
When to use .parse()
Use .parse() when you’re in a context where you genuinely expect the data to be correct and a failure means something has gone horribly wrong. This is often the case in scripts, during build processes, or in serverless functions where you’d want the entire invocation to fail fast and log the error aggressively. It’s the “happy path or bust” approach.
The Graceful .safeParse()
Now, .safeParse() is the diplomat. It doesn’t throw a tantrum. Instead, it returns an object that contains either the success or the failure. This is your go-to for any user-facing or request-handling code where a validation error is just one possible outcome, not a catastrophic failure. It’s how you build robust APIs and forms.
const result = UserSchema.safeParse(badData);
if (result.success) {
// TypeScript knows 'result.data' is the valid user object here.
console.log(`Welcome, ${result.data.name}!`);
} else {
// TypeScript knows 'result.error' is a ZodError here.
console.error("Please correct the following errors:", result.error.format());
// .format() gives you a nested, user-friendly error object:
// {
// name: { _errors: [] },
// age: { _errors: [ "Expected number, received string" ] }
// }
}
The result object is a discriminated union. The success property is a boolean that acts as the switch: if it’s true, you get a data property; if it’s false, you get an error property. This pattern makes TypeScript’s type narrowing incredibly happy and your code rock solid.
Why .safeParse() is Usually the Better Choice
In most real-world applications, especially on the web, data validation is a control flow mechanism, not an exception mechanism. You don’t want your entire HTTP request handler to crash because a user typed a letter in a number field; you want to catch that, tell them about it, and let them try again. .safeParse() gives you that graceful degradation. It allows you to collect all errors and present them back to the user, which is infinitely better than them seeing a generic “500 Internal Server Error” because you threw an uncaught exception.
A Common Pitfall: Forgetting the Discriminant
The one gotcha with .safeParse() is that you must check the success flag. Don’t just assume you can access result.data. TypeScript will rightfully yell at you if you try, because that property doesn’t exist on the error branch. This is a feature, not a bug—it forces you to handle both cases.
const result = UserSchema.safeParse(badData);
// ❌ Bad - This will cause a runtime error if validation fails!
// console.log(result.data.name);
// ✅ Good - Check the state first.
if (result.success) {
// Now it's safe to access
console.log(result.data.name);
}
So, the rule of thumb is simple: use .parse() when you’re a pessimist and want to fail loudly, and use .safeParse() when you’re a realist and want to handle problems gracefully. Most of the time, in the messy world of web development, you’re a realist.