Error Handling without Exceptions

后端 未结 7 1253
夕颜
夕颜 2021-02-05 06:02

While searching SO for approaches to error handling related to business rule validation, all I encounter are examples of structured exception handling.

MSDN and many oth

7条回答
  •  無奈伤痛
    2021-02-05 06:38

    I think I'm close to being convinced that throwing exceptions is actually the best course of action for validation type operations, and especially aggregate type operations.

    Take as an example, updating a record in a database. This is an aggregate of many operations that may individually fail.

    Eg.

    1. Check there is a record in the DB to update. -- record may not exists
    2. Validate the fields to be updated. -- the new values may not be valid
    3. Update the database record. -- the db may raise additional errors

    If we are tying to avoid exceptions, we may instead want to use an object that can hold a success or error state:

    public class Result {
      public T Value { get; }
      public string Error { get; }
    
      public Result(T value) => Value = value;
      public Result(string error) => Error = error;
    
      public bool HasError() => Error != null;
      public bool Ok() => !HasError();
    }
    

    Now we can use this object in some of our helper methods:

    public Result FindRecord(int id) {
      var record = Records.Find(id);
      if (record == null) return new Result("Record not found");
      return new Result(record);
    }
    
    public Results RecordIsValid(Record record) {
      var validator = new Validator(record);
      if (validator.IsValid()) return new Result(true);
      return new Result(validator.ErrorMessages);
    }
    
    public Result UpdateRecord(Record record) {
      try {
        Records.Update(record);
        Records.Save();
        return new Result(true);
      }
      catch (DbUpdateException e) {
        new Result(e.Message);
      }
    }
    

    Now for our aggregate method that ties it all together:

    public Result UpdateRecord(int id, Record record) {
      if (id != record.ID) return new Result("ID of record cannot be modified");
    
      var dbRecordResults = FindRecord(id);
      if (dbRecordResults.HasError())
        return new Result(dbRecordResults.Error);
    
      var validationResults = RecordIsValid(record);
      if (validationResults.HasError())
        return validationResults;
    
      var updateResult = UpdateRecord(record);
      return updateResult;
    }
    

    Wow! what a mess!

    We can take this one step further. We could create subclasses of Result to indicate specific error types:

    public class ValidationError : Result {
      public ValidationError(string validationError) : base(validationError) {}
    }
    
    public class RecordNotFound: Result {
      public RecordNotFound(int id) : base($"Record not found: ID = {id}") {}
    }
    
    public class DbUpdateError : Result {
      public DbUpdateError(DbUpdateException e) : base(e.Message) {}
    }
    

    Then we can test for specific error cases:

    var result = UpdateRecord(id, record);
    if (result is RecordNotFound) return NotFound();
    if (result is ValidationError) return UnprocessableEntity(result.Error);
    if (result.HasError()) return UnprocessableEntity(result.Error);
    return Ok(result.Value);
    

    However, in the above example, result is RecordNotFound will always return false as it is a Result, whereas UpdateRecord(id, record) return Result.

    Some positives: * It bascially does works * It avoids exceptions * It returns nice messages when things fail * The Result class can be as complex as you need it to be. Eg, Perhaps it could handle an array of error messages in the case of validation error messages. * Subclasses of Result could be used to indicate common errors

    The negatives: * There are conversion issues where T may be different. eg. Result and Result * The methods are now doing multiple things, error handling, and the thing they are supposed to be doing * Its extremely verbose * Aggregate methods, such as UpdateRecord(int, Record) now need to concern them selves with the results of the methods they call.

    Now using exceptions...

    public class ValidationException : Exception {
      public ValidationException(string message) : base(message) {}
    }
    
    public class RecordNotFoundException : Exception  {
      public RecordNotFoundException (int id) : base($"Record not found: ID = {id}") {}
    }
    
    public class IdMisMatchException : Exception {
      public IdMisMatchException(string message) : base(message) {}
    }
    
    public Record FindRecord(int id) {
      var record = Records.Find(id);
      if (record == null) throw new RecordNotFoundException("Record not found");
      return record;
    }
    
    public bool RecordIsValid(Record record) {
      var validator = new Validator(record);
      if (!validator.IsValid()) throw new ValidationException(validator.ErrorMessages)
      return true;
    }
    
    public bool UpdateRecord(Record record) {
      Records.Update(record);
      Records.Save();
      return true;
    }
    
    public bool UpdateRecord(int id, Record record) {
      if (id != record.ID) throw new IdMisMatchException("ID of record cannot be modified");
    
      FindRecord(id);
      RecordIsValid(record);
      UpdateRecord(record);
      return true;
    }
    

    Then in the controller action:

    try {
      UpdateRecord(id, record)
      return Ok(record);
    }
    catch (RecordNotFoundException) { return NotFound(); }
    // ...
    

    The code is waaaay simpler... Each method either works, or raises a specific exception subclass... There are no checks to see if methods succeeded or failed... There are no type conversions with exception results... You could easily add an application wide exception handler that returns the the correct response and status code based on exception type... There are so many positives to using exceptions for control flow...

    I'm not sure what the negatives are... people say that they're like GOTOs... not really sure why that's bad... they also say that the performance is bad... but so what? How does that compare to the DB calls that are being made? I'm not sure if the negatives are really valid reasons.

提交回复
热议问题