Cats Core: ValidatedNec and the Real Difference Between Applicative and Monad
In part 1 I went from optional fields to validation errors you can actually act on. This part asks why that error collection works at all, and why it stops working the moment one check depends on another.
Most of my day is Java and Spring Boot, where validation means Bean Validation annotations or a hand-rolled list of error messages. I wanted to see how the Scala side handles it, so I wrote a small playground with cats: a Todo with a title and a description, and a validator that tells you everything that is wrong with it.
The interesting part was not the validation itself. It was why the type I used, ValidatedNec, deliberately refuses to give you flatMap. Following that thread is the clearest explanation of Applicative versus Monad I have found.
The playground
Scala 2.13.18, cats-core 2.13.0. The errors are a small ADT:
final case class Todo(title: String, description: String)
sealed trait TodoError { def message: String }
object TodoError {
case object TitleMissing extends TodoError { val message = "title missing" }
case object DescriptionMissing extends TodoError { val message = "description missing" }
case object TitleToolong extends TodoError { val message = "title too long" }
}Each rule is a small function that returns ValidatedNec[TodoError, String], and the full validation combines them with mapN:
def validateTodo(todo: Todo) =
(validateTitle(todo.title), validateDescriptionMissing(todo.description)).mapN(Todo(_, _))
def validateTitle(title: String): ValidatedNec[TodoError, String] =
(validateTitleLength(title), validateTitleMissing(title)).mapN((_, _) => title)
def validateTitleLength(title: String): ValidatedNec[TodoError, String] =
if (title.trim.length > 10) TitleToolong.invalidNec else title.validNecRunning it with a title of 30 spaces and a real description prints exactly one error:
Invalid(Chain(TitleMissing))It prints Chain, not NonEmptyChain, because NonEmptyChain is a zero-cost newtype over Chain and uses its toString. The non-emptiness lives in the type, not in the runtime object.
Two kinds of box
Here is the distinction that made it click for me.
An Applicative combines boxes that are independent. With (validateTitle(t), validateDescription(d)).mapN(Todo), both boxes exist before anything is combined. Checking the title never needs the description, and the reverse is also true. Because every branch has already run, nothing stops you from combining all the failures. That is what mapN, product and combine do.
A Monad adds flatMap: F[A] and a function A => F[B]. The second box is built from the result of the first. If the first step fails, there is no A to pass in, so step two cannot run. The first error is the only error, by construction. Short-circuiting is not a design choice of Either. It follows from the shape of flatMap.

Same input, four outcomes
To check this, I ran one bad todo through four combinators. The title is too long and the description is blank:
(title(t), description(d)).mapN(Todo)
// Invalid(Chain(TitleTooLong, DescriptionMissing))
for { a <- title(t).toEither; b <- description(d).toEither } yield Todo(a, b)
// Left(Chain(TitleTooLong))
(title(t).toEither, description(d).toEither).parMapN(Todo)
// Left(Chain(TitleTooLong, DescriptionMissing))
for { a <- title(t); b <- description(d) } yield Todo(a, b)
// error: value flatMap is not a member of cats.data.ValidatedNec[...]The data is identical each time. Only the combinator changes, and with it the number of errors you get back. The for comprehension over Either desugars to flatMap, so it stops at TitleTooLong. parMapN borrows Validated's applicative through cats' Parallel instance, so it accumulates again. The last one does not compile.

Why Validated has no Monad
That compile error is deliberate. cats expects a type's ap to agree with the one derived from flatMap. If Validated had a lawful flatMap, it would have to short-circuit, and its mapN would then have to short-circuit as well. You would lose accumulation, which is the whole reason to use the type. So cats gives Validated an Applicative and stops there.
When you really do need a dependent step, you have two exits:
andThenonValidated: aflatMapunder a different name, there to remind you that you just opted into fail-fast.title("Groceries").andThen(t => t.toLowerCase.validNec)givesValid(groceries).toEither: switch toEitherfor the dependent part, thentoValidatedto come back.
The rule I now apply: use Validated for independent checks on input, such as form fields or request bodies. Use Either for pipelines where step two needs step one, such as parsing an ID and then loading the entity.
Why Nec and not Nel
cats ships both ValidatedNel (NonEmptyList) and ValidatedNec (NonEmptyChain). Accumulating errors means appending the right-hand errors to the left-hand ones, over and over. On a List, appending costs time proportional to the length of the left side. A Chain is a tree of concatenations, so appending is constant time. For a handful of form errors you will never notice. It is still the right default, and it is why cats' own documentation recommends Nec.
An aside: validate the value you keep
Writing this up, I found a small bug in my own playground. validateTitleLength checks title.trim, but validateTitle returns the original title. A title of " short" passes and is stored with its leading spaces. The fix is to have the validator return the normalised value (title.trim.validNec), so the data you checked is the data you keep. This is "parse, don't validate" on a small scale.
Takeaway
The question to ask is not "which type should I use". It is does step two need the result of step one? If not, you have an Applicative and you can keep every error. If it does, you have a Monad and the first failure ends the chain. ValidatedNec just makes the type system enforce the answer.