Whetstone.
DDD from zero, in a language you knowErrors as values, not exceptions
Module 1 · Lesson 512 min

Errors as values, not exceptions

In Nest and Next, failure is an exception. A validation fails, you throw new BadRequestException(); something’s missing, you throw new NotFoundException(); a filter catches it upstream and turns it into an HTTP response. It works, and it’s the default everywhere in the ecosystem.

DDD (and Fusion) models failure differently: as a value you return, not an exception you throw. A function that can fail returns a Result: either a success carrying a value, or a failure carrying a list of errors. The caller inspects the Result and decides what to do. No try/catch for expected, everyday validation.

The reason isn’t taste, it’s the type system. A function that throws looks identical to one that can’t fail: function parse(s: string): Note gives you no hint that it might blow up, and whether the caller catches is invisible until something breaks in production. A function that returns Result<Note> says, right there in the signature, “this can fail, and you must handle the failure before you can touch the value.” It moves the question “did you handle the error?” from a runtime hope to a compile-time check.

Two refinements matter, and both are things the Fusion reviewers pushed on you specifically (you’ll see the real comments in the final module).

Accumulate, don’t fail-fast. When you validate several independent rules, collect all the failures and return them together, rather than throwing on the first. A user with three bad fields should see three errors and fix them in one pass, not play whack-a-mole: fix one, resubmit, discover the next, resubmit again. The returned-value shape makes this natural (push each broken rule into a list, return success only if the list is empty); the throw-on-first-exception shape actively fights it.

Type the errors; don’t branch on strings. An endpoint that must return 404 for a missing entity and 504 for a timeout needs to tell those two failures apart. Do it on the error’s type (an EntityNotFound versus a Timeout), never on its message string. if (error.message.includes("not found")) is prose masquerading as logic: it breaks the instant someone rewords the message, and it can’t distinguish two failures that share a word. A typed error is a contract; the caller pattern-matches the kind of failure and maps each to the right response. This is the “lift the distinction into the type system” move, and it’s exactly what a reviewer asked you to do when you’d collapsed several distinct failures into one generic “warning.”

That’s the last foundation concept. You now have the four moves in plain form: name things with the domain’s language, split entities from value objects by identity, let aggregates own their invariants instead of draining them into services, and return errors as typed, accumulating values. The next module drops into the real Fusion code and shows the exact Result<T> machinery that implements this last idea, then the rest of the course builds up the others, and the final module walks the real reviews where you learned each one the hard way.

Practice

Try it yourself

Recall

Why return errors instead of throwing

State the benefit the type system gives you.

What does returning errors as values (instead of throwing exceptions) buy you that try/catch doesn't?

Reveal answer

The failure becomes visible in the type. A function that returns Result tells the caller, at compile time, that it can fail and forces them to handle it before touching the value. A function that throws looks identical to one that can't fail; whether the caller catches is invisible until runtime. Errors-as-values moves 'did you handle the failure?' from a runtime hope to a compile-time check.

Quiz

Return or throw

Which of these should be a returned failure, not a thrown exception?

  1. AA user submits a note body over the length limit
  2. BA required field is missing from a request
  3. CA switch over a known enum hits a 'this should be unreachable' arm
  4. DBoth the first and second
Show answer

Correct answer: D — Both the first and second

Invalid user input (too long, missing required field) is EXPECTED and routine: it's data the outside world hands you every day, so it's a returned failure the caller handles. An unreachable switch arm is a BUG (an impossible state actually happened), so it throws loudly. The rule: Result for invalid data, exceptions for impossible states.

Do

Accumulate, don't fail-fast

Feel the difference between validation that stops at the first error and validation that reports all of them. Scratch file or paper.

  • Picture a class-validator DTO with three bad fields. Note that a fail-fast validator (throw on the first) makes the user fix one, resubmit, hit the next, resubmit again: whack-a-mole.
  • Now picture a function that checks all three rules and returns a list of every failure at once: { ok: false, errors: [e1, e2, e3] }.
  • Write that shape in TS: a function that pushes each broken rule into an errors array, then returns success only if the array is empty.
  • Confirm the accumulating version reports every problem in one pass, so the caller fixes everything at once
Done whenYou wrote a validator that collects all failures into a list and returns success only when the list is empty, and you can see why accumulating beats fail-fast for user-facing validation.
Recall

Why typed errors, not strings

A distinction the reviewers pushed on you directly.

An endpoint must return 404 for a missing entity and 504 for a timeout. Why should it branch on the error's TYPE rather than its message string?

Reveal answer

Because the type is a contract and the string is prose. if (error.message.includes('not found')) breaks the moment someone rewords the message, and it can't distinguish two failures that happen to share a word. A typed error (EntityNotFound vs Timeout) lets the caller pattern-match the KIND of failure and map each to the right response. Typed errors lift the distinction the domain cares about into the type system.

Sign in to track your progress →