1

I am writing some validation code and am not sure how to pass validation messages back to the calling code.

Exceptions come to mind, but I think that Exceptions should not be used in user input validation. As @Blowdart puts it:

Exceptions are not control flow mechanisms. Users often get passwords wrong, it's not an exceptional case. Exceptions should be a truly rare thing, UserHasDiedAtKeyboard type situations.

From: https://stackoverflow.com/a/77175/125938. I'm extending that sentiment to all "incorrect" user input that a user might enter.

So the question is what to use instead of Exceptions. For certain situations, I could just use an IsValid… method that returns a bool for validity, but what if I want to pass an error message back with it? Should I create a custom "ValidationError" object, with a Message property? What makes sense and causes Least Astonishment (preferably a tried and tested pattern)?

Protector one
  • 6,926
  • 5
  • 62
  • 86

3 Answers3

1

If I were to do this in a truly object-oriented way, I'd adhere to the separation of concerns principle and compose a chain of classes that each deal with a separate step on the input - validation - output journey.

Let's say we are parsing a date from string as entered by the user.

My first class would encapsulate the raw value and attempt to parse the date (pseudo-code):

class TextualDate {
   public TextualDate(string value) {
      // just initialize with the provided value
   }

   public Option<Date> AsDate() {
      // try parsing and either return the date or not
      // the Option<Date> type is here to suggest that the conversion might not succeed
   }
}

Next I'd have a validation class that instantiates the TextualDate class, invokes its AsDate() method and returns the validation result:

class ValidatedDate {
  public ValidatedDate(TextualDate value) {
    // initialize with the provided value
   _textualDate = value;
  }

  private TextualDate _textualDate;

  public ValidationResult Validated {
    var maybeDate = _textualDate.AsDate();
    // see whether we have a date or not
    return new ValidationResult(...);
  }
}

In our ValidationResult class, we might find some status property (OK, Failed), any error message either provided directly or as a key to then look-up in a message catalog etc.

This way, we can isolate concerns and only deal with the error messages on the UI layer while being able to use and reuse the validation logic independently.

Tomas Kohl
  • 1,388
  • 1
  • 19
  • 27
  • This sounds pretty good, but am I understanding it correctly that you would use this as, for example: `new ValidatedDate(new TextualDate(Form["date"])).Validated.Valid`? That's a bit verbose. – Protector one Mar 07 '19 at 14:39
  • I don't mind verbose if I can keep things small, focused, and testable. Arguably this is a matter of taste, too. – Tomas Kohl Mar 08 '19 at 07:40
1

Users often get passwords wrong, it's not an exceptional case.

Yes and no. Whether to throw an exception or not depends on the question you're asking. And in the course of logging a user in, there are typically quite a number of questions being asked before you come to the conclusion whether the user can be logged in or not. The more you break down your code into specialised parts, the more it may make sense to raise exceptions in some of those parts.

Say you specify your login procedure the following way in an HTTP context:

  1. Get the username* and password* from the request.
  2. Fetch the user record* by its username from the database*.
  3. Check whether the record's password* equals* the entered password.
  4. If yes, start a session.
  5. If any of the above steps do not successfully complete, output an appropriate error message.

Any of the items marked with an asterisk above may fail:

  1. The request may not contain a username or password.
  2. There may not be a user record for this username, or the database may be down.
  3. For whatever reason, the record may not have a password and/or be corrupted. The stored password may, for whatever reason, use an unsupported hashing algorithm and hence can't be compared.

It should be rather obvious that in this process there are any number of cases that would be ideal to be implemented as an exception. The actual function which tests the password should probably not throw an exception in case the password is merely false; that should be a boolean return value. But it may still throw an exception for any other number of reasons. If you use exceptions properly, you'll end up with code that looks something like this (pseudo-pseudo code):

try {
    username = request.get('username')
    password = request.get('password')
    user = db.get(username=username)
    if (user.password.matches(password)) {
        session.start()
    } else {
        print 'Nope, try again'
    }
} catch (RequestDoesNotHaveThisDataException) {
    logger.info('Invalid request')
    response.status(400)
} catch (UserRecordNotFoundException) {
    print 'Nope, try again'
} catch (UnsupportedHashingAlgorithmException, PasswordIsNullException) {
    logger.error('Invalid password hash for user ' + user.id)
    response.status(500)
    print 'Sorry, please contact our support staff'
} catch (DatabaseDownException e) {
    // mostly for illustration purposes, 
    // this exception should probably not even be caught here
    logger.exception('SEND HALP!')
    throw e
}

So, yes, this is a very simple process, but literally every step along the way has one or more exceptional cases. You ask the question "what is the username the user sent in the request?", and if there's no answer to this question because the user didn't sent any username, you have an exceptional case. Exceptions simplify control flow here a lot as opposed to trying to cover each of these cases with an if..else.

It is NOT an exception if the username is not valid or the password is not correct.
(From the answer you quote from.)

As you can see, we're testing whether the username is "valid" or not by trying to fetch its record from the database. If we have a function whose purpose is to fetch records of users from the database, and there is no such record, then an exception is an entirely valid response. If we defined that function to test whether such a record exists and null or false is a valid return value… fine. But in this case we didn't write it that way, and frankly, that results in simpler control flow I find.

Now, only the password validation itself does not use an exception, since the question asked there is "does this password match that password?", to which the answer can clearly be yes or no. Again, only if something exceptional like an unsupported hashing algorithm turns up can there be no answer to this question and an exception is entirely warranted.

Having said all this, you may notice that most of these cases, except the really fatal one with the database, does not outwardly result in an exception. The component here is expecting and handling certain cases that its sub-components regard as exceptional. This code here is asking the questions, and is prepared to handle Mu as an answer for some of them. Which is to say, a general rule that says "exceptions shouldn't be used in process X, Y or Z because it's not exceptional enough" is too dogmatic. It depends on the purpose of each individual piece of code whether an exception is warranted or not.


Having said all this, what you're asking about is some sort of form validation. The above code shows a case where two pieces of data may each be invalid, and it's using exceptions to in the end still result in a "yes" or "no" response. You can of course encapsulate that in an object like this:

val = new LoginFormValidator()
val.setDataFromRequest(request)
val.validate()

if (val.isValid) {
    print 'Hurray'
} else {
    print 'You have errors:'

    for (error in val.errors) {
        print error.fieldName + ': ' + error.reason
    }
}

Whether this validator uses exceptions internally for any of this you do not need to care, but in the end it saves all of them as a "yes" or "no" result to its internal properties, from where you can take them either as an aggregate (val.isValid) or individually (for (error in val.errors)).

deceze
  • 510,633
  • 85
  • 743
  • 889
1

I was faced with a similar dilemma in the past - I had to write a couple of services that takes data from a 3rd party, manipulating it in various ways, and send that data to other services for further processing.
All of these services might have failed because of wrong or incomplete data, but it was nor unexpected neither exceptional - and I refused to use exceptions for these cases.
I've done an extensive research, reading anything I could get my hands on in this subject in two days, and finally decided the following:

A method might need to return data and might not (sub in Visual Basic, void in Java/C#) - but in both cases, I wanted an indication for success/fail and a potential error message.

If your language of choice supports tuples, you could return a tuple from your methods:

public (bool Success, string ErrorMessage) DoSomething()
{
    // implementation here
}

or

public (bool Success, someType Value, string ErrorMessage) DoSomething()
{
    // implementation here
}

If not, you can do what I did (That was c#5 - so no value tuples) and create a result class:

public class Result
{
    public static Result Success()
    {
        return new Result(true, null);
    }

    public static Result Fail(string errorMessage)
    {
        return new Result(false, errorMessage);
    }

    protected Result(bool success, string errorMessage)
    {
        Success = success;
        ErrorMessage = errorMessage;
    }

    public bool Success {get; private set;}
    public string ErrorMessage {get; private set;}  
}

public class Result<T>
{
    public static Result<T> Success(T value)
    {
        return new Result(true, null, value);
    }

    public new static Result<T> Fail(string errorMessage)
    {
        return new Result(false, errorMessage, default(T));
    }

    private Result<T>(bool success, string errorMessage, T value)
        : base(success, errorMessage)
    {
        Value = value;
    }

    public T Value {get; private set;}
}

And use it like this:

public Result CouldBeVoid()
{
    bool IsOk;
    // implementation

    return IsOk ? 
    Result.Success() : 
    Result.Fail("Something went wrong") ;

}


public Result<int> CouldBeInt()
{
    bool IsOk;
    // implementation

    return IsOk ? 
    Result.Success(intValue) : 
    Result.Fail("Something went wrong") ;
}


var result = CouldBeVoid();
if(!result) 
    // do something with error message

var result = CouldBeInt()

if(result)
    // do something with int value
else
    // do something with error message
Zohar Peled
  • 79,642
  • 10
  • 69
  • 121