Topics covered:

  • What is Defensive Programming?
  • Assertions and Debug.Assert(…)
  • Exceptions Handling Principles
  • Error Handling Strategies

Video (in Bulgarian)

Presentation Content

Defensive programming

Programming today is a race between software engineers striving to build bigger and better idiot-proof programs, and the Universe trying to produce bigger and better idiots. So far, the Universe is winning.

- Rick Cook, The Wizardry Compiled

What is Defensive Programming?

  • Similar to defensive driving – you are never sure what other drivers will do
  • Expect incorrect input and handle it correctly
  • Think not only about the usual execution flow, but consider also unusual situations!

Protecting from Invalid Input

  • “Garbage in → garbage out” – Wrong!
    • Garbage in → nothing out / exception out / error message out / no garbage allowed in
  • Check the values of all data from external sources (from user, file, internet, DB, etc.)
  • Check the values of all routine input parameters
  • Decide how to handle bad inputs
    • Return neutral value
    • Substitute with valid data
    • Throw an exception
    • Display error message, log it, etc.
  • The best form of defensive coding is not inserting error at first place

Assertions

  • Assertion – a statement placed in the code that must always be true at that moment
    • Assertions are used during development
      • Removed in release builds
    • Assertions check for bugs in code
public double GetAverageStudentGrade()
{
  Debug.Assert(studentGrades.Count > 0,
     "Student grades are not initialized!");
  return studentGrades.Average();
}
  • Use assertions for conditions that should never occur in practice
    • Failed assertion indicates a fatal error in the program (usually unrecoverable)
  • Use assertions to document assumptions made in code (preconditions & postconditions)
private Student GetRegisteredStudent(int id)
{
        Debug.Assert(id > 0);
        Student student = registeredStudents[id];
        Debug.Assert(student.IsRegistered);
}
  • Failed assertion indicates a fatal error in the program (usually unrecoverable)
  • Avoid putting executable code in assertions
Debug.Assert(PerformAction(), "Could not perform action");
  • Won’t be compiled in production. Better use:
bool actionPerformed = PerformAction();
Debug.Assert(actionPerformed, "Could not perform action");
  • Assertions should fail loud
    • It is fatal error → total crash

Exceptions

  • Exceptions provide a way to inform the caller about an error or exceptional events
    • Can be caught and processed by the callers
  • Methods can throw exceptions:
public void ReadInput(string input)
{
  if (input == null)
  {
    throw new ArgumentNullException("input");  }
  …
}
  • Use try-catch statement to handle exceptions:
  • You can use multiple catch blocks to specify handlers for different exceptions
  • Not handled exceptions propagate to the caller
void PlayNextTurn()
{
  try
  {
    readInput(input);
    …
  }
  catch (ArgumentException e)
  {
    Console.WriteLine("Invalid argument!");
  }
}
  • Exception thrown here
  • The code here will not be executed
  • Use finally block to execute code even if exception occurs (not supported in C++):
  • Perfect place to perform cleanup for any resources allocated in the try block
void PlayNextTurn()
{
  try
  {
    …
  }
  finally
  {
    Console.WriteLine("Hello from finally!");
  }
}
  • Exceptions can be eventually thrown here
  • The code here is always executed
  • Use exceptions to notify the other parts of the program about errors
    • Errors that should not be ignored
  • Throw an exception only for conditions that are truly exceptional
    • Should I throw an exception when I check for user name and password? → better return false
  • Don’t use exceptions as control flow mechanisms
  • Throw exceptions at the right level of abstraction
class Employee
{
    // Badpublic TaxId
  { get { throw new NullReferenceException(…); }
}
class Employee
{
    // Betterpublic TaxId
  { get { throw new EmployeeDataNotAvailable(…); }
}
  • Use descriptive error messages
    • Incorrect example:
      throw new Exception("Error!");
      
    • Example:
      throw new ArgumentException("The speed should be a number between " + MIN_SPEED + " and " + MAX_SPEED + ".");
      
    • Avoid empty catch blocks
try
{
  …
}
catch (Exception ex)
{
}
  • Always include the exception cause when throwing a new exception
try
{
    WithdrawMoney(account, amount);
}
catch (DatabaseException dbex)
{
      throw new WithdrawException(String.Format(
        "Can not withdraw the amount {0} from acoount {1}",
        amount, account), dbex);
}
  • We chain the original exception (the source of the problem)
  • Catch only exceptions that you are capable to process correctly

    • Do not catch all exceptions!

    • Incorrect example:

      try
      {
        ReadSomeFile();
      }
      catch
      {
        Console.WriteLine("File not found!");
      }
      
    • What about OutOfMemoryException?

  • Have an exception handling strategy for all unexpected / unhandled exceptions:

    • Consider logging (e.g. Log4Net)
    • Display to the end users only messages that they could understand

Error Handling Techniques

  • How to handle errors that you expect to occur?
    • Depends on the situation:
      • Throw an exception (in OOP)
        • The most typical action you can do
      • Return a neutral value, e.g. -1 in IndexOf(…)
      • Substitute the next piece of valid data (e.g. file)
      • Return the same answer as the previous time
      • Substitute the closest legal value
      • Return an error code (in old languages / APIs)
      • Display an error message in the UI
      • Call method / Log a warning message to a file
      • Crash / shutdown / reboot

Assertions vs. Exceptions

  • Exceptions are announcements about error condition or unusual event
    • Inform the caller about error or exceptional event
    • Can be caught and application can continue working
  • Assertions are fatal errors
    • Assertions always indicate bugs in the code
    • Can not be caught and processed
    • Application can’t continue in case of failed assertion
  • When in doubt → throw an exception

Assertions in C#

  • Assertions in C# are rarely used
    • In C# prefer throwing an exception when the input data / internal object state are invalid
      • Exceptions are used in C# and Java instead of preconditions checking
    • Prefer using unit testing for testing the code instead of postconditions checking
  • Assertions are popular in C / C++
    • Where exceptions & unit testing are not popular
  • In JS there are no built-in assertion mechanism

Error Handling Strategy

  • Choose your error handling strategy and follow it consistently
    • Assertions / exceptions / error codes / other
  • In C#, .NET and OOP prefer using exceptions
    • Assertions are rarely used, only as additional checks for fatal error
    • Throw an exception for incorrect input / incorrect object state / invalid operation
  • In JavaScript use exceptions: try-catch-finally
  • In non-OOP languages use error codes

Robustness vs. Correctness

  • How will you handle error while calculating single pixel color in a computer game?
  • How will you handle error in financial software? Can you afford to lose money?
  • Correctness == never returning wrong result
    • Try to achieve correctness as a primary goal
  • Robustness == always trying to do something that will allow the software to keep running
    • Use as last resort, for non-critical errors

Assertions vs. Exceptions

public string Substring(string str, int startIndex, int length)
{
  if (str == null)
  {
    throw new NullReferenceException("Str is null.");
  }
  if (startIndex >= str.Length)
  {
    throw new ArgumentException(
      "Invalid startIndex:" + startIndex);
  }
  if (startIndex + count > str.Length)
  {
    throw new ArgumentException("Invalid length:" + length);
  }
  …
  Debug.Assert(result.Length == length);
}
  • Check the input and preconditions
  • Perform the method main logic
  • Check the postconditions

Error Barricades

  • Barricade your program to stop the damage caused by incorrect data
  • Consider same approach for class design
    • Public methods → validate the data
    • Private methods → assume the data is safe
    • Consider using exceptions for public methods and assertions for private
  • public methods / functions
  • private methods / functions

Being Defensive About Defensive Programming

  • Too much defensive programming is not good
    • Strive for balance
  • How much defensive programming to leave in production code?
    • Remove the code that results in hard crashes
    • Leave in code that checks for important errors
    • Log errors for your technical support personnel
    • See that the error messages you show are user-friendly