DDD from zero, in a language you knowErrors as values, not exceptions
No narration yet
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?

Quiz

Return or throw

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

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.

Tick every step to confirm you did it.

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?