I've already said that in CB, but repeating it here for the benefit of others:
the error is getting set as expected, but it croaks rather than gets encapsulated somewhere
That part puzzles me. If a validation fails, I do expect it to throw an error. That gives you the opportunity to catch the exception wherever you want in your code, and deal with it there.
If it didn't throw the exception, but encapsulated it somewhere, would the operation just fail silently if the validation failed? And what would the calling code make of that? That doesn't strike me as a useful modus operandi.
(Disclaimer: I've never worked with dbic::Result::Validation, I'm merely applying my common sense here).
Update: Answer to a new question after OP's update:
However, I still can't work out how to get to the error data
I'd guess that the exception is an objet of type DBIx::Class::Result::Validation::VException, and you can use the message and object accessors.