I like this pattern a lot. On a recent project, I used it almost exactly, but with a richer model of 'error'. Even though it was just basic form validation, I thought it led to very clean code.
One criterion you could apply is that exceptions should only be thrown for violations of the code's published expectations that can not be statically prevented. Whether a validation failure would be "unexpected" then depends on whether the code is expressly validation code that expects unvalidated data (in which case validation failures would not be unanticipated) or processing code which "expects" good data and does something with good data, but throws an exception on bad data -- note that in the same workflow, at different levels, both models could be used, with different levels creating or swallowing exceptions depending on the expectations of the particular function.