What are errors?
Errors are mistakes in the code. They can range from typos, incorrect use of variables and functions that can be spotted right away, to those that occur while the program is being executed. They often depend on the sequence of the execution and the results of previously executed code.
Let's review how to create and use an application. You write it in a programming language which is then translated into machine code and then executed as you use the application.
Here are two phases in this process:
Compilation (or interpretation in some languages) - this is where the code is being verified and translated to machine code. The result is a file that is ready to be executed and is called - executable.
Execution - running the application or executing the executable!
Each of the phases can have its own set of errors.
Spot and fix compilation errors
Compilation errors can be detected by the C# compiler (csc.exe) during the process of translating your C# code into machine-readable byte code. These errors can be syntactic such as incorrect spelling. They can also be semantic - such as the use of forbidden words, non-interpretable code, incorrect types, etc.
If you use an integrated development environment (IDE) such as Visual Studio, you will be notified of errors in live mode while you type your code. The lines of code that correspond to compilation errors are highlighted. These are "easy" errors as they can (and must) be fixed on the spot. The application can't be compiled until there are no compilation errors. It's easy to fix, and you can see exactly what's wrong.
On the other hand, runtime errors can be challenging.
Manage runtime errors
Runtime errors occur during the execution process after the app is launched. They appear only during the execution process, and there's no way to detect them during compilation.
This kind of error is typically related to the logic of the code, such as accessing data that doesn't exist or that exists in a different format, or performing an unsupported action, etc.
For example, if you are trying to access an element of a list at an index that's greater than the length of that list, then there's an error in the logic: You thought the list would be longer.
An example of a business logic error could be that you reversed the meaning of credit and debit (no good) in a finance application. However, the application will still manage to work in this situation. It will just produce the complete opposite of what you need! So, don't mix them up! 😉
A logic error causes the application to crash.
Some of these errors can be easily reproduced as they happen consistently. Others occur only occasionally and are more difficult to fix as they are harder to reproduce. Occasional errors are usually related to specific conditions in the code. Here's an example of this kind of error in the code outline:
if (weekDay)
{
//run ERROR FREE CODE
}
else
{
//run CODE WITH ERRORS
}
In the example above, if you are working on your project only Monday to Friday and get all the reported crashes over the weekend, it won't be evident at first why your app is crashing.
So, what can you do about it?
In some cases, you can debug your program and detect those weak spots.
In other cases, you know in advance that some places in your code may be weak, due to things such as external dependencies (parts of a device, other applications, external storage, network, etc.).
To address those, you can develop your program to work with them (and not against them 🙂). You can generally use two strategies:
Test for errors using conditions.
Leverage the exceptions mechanism.
Let's review each strategy with a common example: The infamous division by zero mistake.
Testing for errors
First, create a utility class inside an Exceptions
namespace called SimpleMath
that provides a CalculateAverage
method that calculates the average of a list of integer values.
using System;
using System.Collections.Generic;
namespace Exceptions
{
public class SimpleMath
{
/// <summary>
/// Calculate the average value of a list of integers
/// </summary>
/// <param name="listOfIntegers">A list containing integer numbers</param>
/// <returns>The average of the list</returns>
public static int CalculateAverage(List<int> listOfIntegers)
{
int average = 0;
foreach (int value in listOfIntegers)
{
average += value;
}
average /= listOfIntegers.Count;
return average;
}
}
}
Then define a program in a class named TemperatureAverage
inside the same Exceptions namespace that makes use of that functionality. This program receives temperature values as command-line arguments, calls the CalculateAverage
function and prints the result.
using System;
using System.Collections.Generic;
namespace Exceptions
{
public class TemperatureAverage
{
/// <summary>
/// Displays the average temperature from values provided as command-line arguments
/// </summary>
/// <param name="args">space-separated list of temperatures</param>
public static void Main(string[] args)
{
List<int> recordedTemperaturesInDegreesCelcius = new List<int>();
// Fill the list from values provided as command-line arguments
foreach(string stringRepresentationOfTemperature in args)
{
int temperature = int.Parse(stringRepresentationOfTemperature);
recordedTemperaturesInDegreesCelcius.Add(temperature);
}
// Calculate and print the average temperature
int averageTemperature =
SimpleMath.CalculateAverage(recordedTemperaturesInDegreesCelcius);
Console.WriteLine("The average temperature is " + averageTemperature);
}
}
}
In this code, first create an empty list, and then iterate over the args array containing the provided command-line arguments. Since each argument is provided as a string
, convert it to an int
using the int.Parse utility method before adding it to the list. Once the list is complete, call the CalculateAverage
function from the SimpleMath
class, assign the result to the averageTemperature
variable, and print its value.
Try it out for yourself!
Ready to get started? To access the exercise, click this link.
Unsurprisingly, you run into trouble if you do not provide any argument on the command-line, since you can't divide by zero. To avoid the error, add a check inside the CalculateAverage
method:
if (listOfIntegers.Count == 0)
{
return 0;
}
However, this strategy would generate a very misleading result: 0
is not a valid result. We haven't even performed the division! We can, however, fix the Main
function to avoid calling an empty list:
using System;
using System.Collections.Generic;
namespace Exceptions
{
public class TemperatureAverageWithCheckForEmptyList
{
/// <summary>
/// Displays the average temperature from values provided as command-line arguments
/// </summary>
/// <param name="args">space-separated list of temperatures</param>
public static void Main(string[] args)
{
List<int> recordedTemperaturesInDegreesCelcius = new List<int>();
// fill the list from values provided as command-line arguments
foreach(string stringRepresentationOfTemperature in args)
{
int temperature = int.Parse(stringRepresentationOfTemperature);
recordedTemperaturesInDegreesCelcius.Add(temperature);
}
// Guard against empty list
if(recordedTemperaturesInDegreesCelcius.Count == 0)
{
Console.WriteLine("Cannot calculate average of empty list!");
}
else
{
// Calculate and print the average temperature
int averageTemperature =
SimpleMath.CalculateAverage(recordedTemperaturesInDegreesCelcius);
Console.WriteLine("The average temperature is " + averageTemperature);
}
}
}
}
Try it out for yourself!
Ready to get started? To access the exercise, click this link.
It looks like there's another problem: The int.Parse
method does not accept "eight" as an input and throws a FormatException exception that crashes the program.
Interesting! It shows a stack trace - a message C# prints when a program crashes. It evens tells you what happened: System.FormatException: Input string was not in correct format.
So, if C# is able to print this message, could it prevent the crash?
It sure can! This is what the exception handling mechanism is for. 🙂
Exception handling
The exception handling mechanism is the default error-management mechanism in C#. It consists of throwing an event that interrupts the normal flow of execution when a problem occurs. If this event is caught, the problem can be dealt with. Otherwise, the program crashes with a stack trace that describes what happened.
A general code model for this is try/catch
. This means that you're requesting to do something - execute a block of code, or, rather, try
to. If an error occurs, you catch
it.
You can handle the error (or errors) in the catch part. It could be as simple as doing nothing or retrying (if your business logic permits). By catching an error (even if you do nothing), you prevent the app from crashing.
Here's what this construction looks like in C#:
try
{
// some code
// a function to try that can generate an error
// more code
}
catch (ExceptionName e) {
// code to execute in case the trial didn't work out and an error happened
}
What happens if we try?
When something goes wrong inside a method while it runs, it throws an error. The exception bubbles up to the chain of method calls until it is caught. If no catch statement is provided, the program ends up crashing.
Let's fix our program to handle known errors:
using System;
using System.Collections.Generic;
namespace Exceptions
{
public class TemperatureAverageWithExceptionHandling
{
/// <summary>
/// Displays the average temperature from values provided as command-line arguments
/// </summary>
/// <param name="args">space-separated list of temperatures</param>
public static void Main(string[] args)
{
try
{
List<int> recordedTemperaturesInDegreesCelcius = new List<int>();
// Fill the list from values provided as command-line arguments
foreach(string stringRepresentationOfTemperature in args)
{
int temperature = int.Parse(stringRepresentationOfTemperature);
recordedTemperaturesInDegreesCelcius.Add(temperature);
}
// Calculate and print the average temperature
int averageTemperature =
SimpleMath.CalculateAverage(recordedTemperaturesInDegreesCelcius);
Console.WriteLine("The average temperature is " + averageTemperature);
}
catch (FormatException e)
{
Console.WriteLine("All arguments should be provided as numbers");
Environment.Exit(-1);
}
catch (DivideByZeroException e)
{
Console.WriteLine("At least one temperature should be provided");
Environment.Exit(-1);
}
}
}
}
This code is divided into two parts:
The normal flow, between
try {
and}
, that consists of the instructions to be performed as long as no error occurs is detected.A set of
catch(Exception e)
instructions where the thrown exception matches the type of exception defined in acatch
statement. The block which corresponds to that catch statement is executed. If no catch statement matches, the program crashes and displays the stack trace.
This mechanism separates the normal flow of a program from the error-handling part. It even allows an error that occurs in a method to be managed in the calling method. In our example, the DivideByZeroException
is thrown from the CalculateAverage
method, but caught in the Main
method.
Try it out for yourself!
Ready to get started? To access the exercise, click this link.
Let's recap!
In this chapter, you've learned a number of concepts regarding application errors:
Compilation is the process of translating code from a programming language to machine code. Compilation errors are easily discoverable and must be resolved for the compilation process to be completed.
Execution is the process of using or running the application. Runtime errors are harder to discover and cause an application to crash.
In C#, runtime errors generate exceptions.
Exceptions are handled by using a try/catch statement:
Try - marks a function that may throw an exception.
Catch - indicates which code to execute upon a thrown exception.
In the next chapter, we will cover communication with the user, which will allow us to dig deeper into exceptions along the way.