In this post, we learn how to debug a C# .NET Console application using the Console messages and Visual Studio Code's debugger. We also learn how to use conditional statements to avoid exceptional situations.
Welcome back to another edition of C# From Scratch, a series of articles designed to teach you everything you need to know to use the C# programming language to create software applications.
In the last part of the series, we learned how to create an interactive C# application by taking arguments at runtime and updating the output of the application based on those arguments. You can find that part along with all other previous parts of the series here.
At the end of the last part, we discovered a bug in our application. After making some updates, we tried to run the application without passing in any arguments and discovered that the application crashed with an unhandled exception. In this part, we will learn how to use Visual Studio Code to debug an application. By the end of this post, you will be comfortable debugging applications in Visual Studio Code, and our application will once again be in a stable state.
Let's start off with a quick recap of where we left off.
In the last part of the series, we discovered that if we run our application without any arguments, the application crashes with an unhandled exception. Let's talk about what this means quickly.
We have mentioned before that the .NET Runtime is responsible for managing your application as it runs on a machine. As the manager of your application, the .NET Runtime is responsible for making sure that your application has enough resources to run correctly. It is also responsible for making sure that your application doesn't do anything obviously wrong. If your application was able to do something obviously wrong, then there is a chance that it could corrupt data on the machine or do some damage to the machine itself.
When the .NET Runtime detects that your application is trying to do something obviously wrong at runtime, then the .NET Runtime will force your application to throw an exception. An exception is an exceptional situation that usually indicates an error condition has occurred. When an exception is thrown, the application has an opportunity to handle the exception. If the exception is handled by the application, then the error condition can be resolved and the application can continue executing.
If the exception is not handled by the application, then it is an unhandled exception. An unhandled exception will eventually crash an application, bringing the execution of the application to a screeching halt. That's because .NET won't allow a faulty application to continue running.
We'll talk about throwing and handling exceptions in much more detail later on. For now, all you need to know is that an unhandled exception is an error condition that indicates something is wrong in your application and stops the execution of the application.
When an unhandled exception occurs, it can be easy or hard to understand the cause and source of the exception. Let's take a look at our application to see if we can figure out what is going wrong.
The Console window is the first place where we learn about unhandled exceptions when running a C# .NET project.
The message on the Console specifies the type of unhandled exception that occurred and the location in the application where it occurred. Most of the time, this information is genuinely useful. The rest of the time, the type of exception can be cryptic and hard to understand and the .NET Runtime can get confused about where exactly in the application the exception occurred.
In this case, we can see that the exception type is System.IndexOutOfRangeException and the help text further explains that this exception is caused by an index that was outside the bounds of the array. That is a very clear error message that tells us exactly what has gone wrong in our application. If we wanted more information about this exception and the situations that cause it, we could search on docs.microsoft.com for an entry explaining the exception. You can think whatever you want about Microsoft, but you have to credit them for having good, complete documentation when you need it.
The second part of the Console message tells us where in our application the exception occurred. We can see that the exception occurred in Program.cs on line 9. Not surprisingly, that is the only line in the application where we attempt to index into an array.
So, from the Console messages, we know exactly what the error is and where it occurred. What happens if the Console messages are not so clear and you can't immediately understand what has caused the exception?
In that case, you would use a debugger to step through and analyze your code line by line to determine what is going wrong. Let's look at how to use the debugger in Visual Studio Code.
When you start learning C#, or any programming language really, it can be very difficult to read your source code and understand exactly where an error is occurring. In situations like this, it is easier to find errors by executing your application in a controlled environment. A controlled environment is one where you can execute the application line by line, or section by section, stopping regularly to inspect the environment and understand the values that variables currently contain.
The debugger in Visual Studio Code lets you do exactly this.
Let's look at how to use the debugger in Visual Studio Code.
When running an application with a debugger, you can pause the execution of your program at a specific line of code to inspect the environment before that line of code is executed. You tell the .NET Runtime that you want to stop at a specific line of code by adding a breakpoint to that line of code in Visual Studio Code. To add a breakpoint to a line, simply click on the margin to the left of the line number. A red dot appears indicating that a breakpoint has been added.
You can remove a breakpoint, by clicking the breakpoint again.
Now, when you run the application with the debugger, the debugger will pause the execution of the application before this line of code. Since we are pausing execution before this line is executed, we can check the value of the args array before the exception occurs. To run your application with the debugger, click on Run > Start Debugging in Visual Studio Code or use the keyboard shortcut F5.
The application runs and halts when the debugger hits our breakpoint. We can see where the execution is halted because the next line of code to execute is highlighted in yellow.
If we hover over the args array or look in the Variables pane, we can see information about this variable. The expression string[0], indicates that this is an array of strings that has zero elements.
Intuitively, we can understand that trying to access the first element in an array with zero elements is a bad idea and will trigger an exception. Now we know what the problem is. We can also verify that this is where the exception occurs by clicking on the Continue button in the debug toolbar or using the keyboard shortcut F5. When you choose to continue, the debugger will continue executing your code until the end of execution is reached or it hits the next breakpoint.
Other buttons on the toolbar allow you to incrementally execute the code in the application in different ways. The Step Over button will not follow execution into a method, whereas the Step Into button will.
Once you continue, the debugger immediately reports that an unhandled exception has occurred.
You can now stop debugging, using the Stop button in the debug toolbar.
Now we know where the exception is occurring and what is causing the exception. So, what will we do about it?
The exception is caused by our code attempting to access an element of an array that doesn't exist because the user hasn't provided the correct input.
You may make the argument that the exception is caused by a user not using our application correctly, however, users will always use your applications incorrectly. As a programmer, it is your job to understand the ways that people might abuse your application and ensure that it works anyway. It is easier to handle exceptions than to change the behaviour of every user on the planet.
Since we don't know how to handle exceptions yet, we can avoid this exceptional situation by checking if a user has provided an argument while running the application. If the user has provided an argument, then we will print a personalized greeting otherwise, we will print a generic greeting.
What we have just described is something called a conditional statement. With a conditional statement, we can change the flow of execution based on whether a certain condition is true or false. In this case, the condition we will check is the length of the args array. If the length of the args array is greater than 0, then the user has provided the application with an argument that we can use to print a personalized greeting.
It turns out that an array has a property called length. We'll talk about properties and methods later when we learn about object-oriented programming. For now, just know that a property is a piece of data that is attached to an object like the args array. We can access an object's properties using the dot operator followed by the name of the property. In this case, we can access the length of args with the expression "args.length"'.
The conditional statement we will use here is an if-else statement. With this type of statement, you check the value of a Boolean expression. If the value is true, then you execute one piece of code, else you execute another block of code.
In C#, an if statement starts with the if keyword followed by the condition to be evaluated in parentheses and a code block to be executed if the condition is true. In this case, we want to check if the length of args is greater than 0. If that condition is true, then we will print the personalized greeting. in C#, you can use the angle bracket operators to check if one value is greater than (>) or less than another (<). You can also check equality using the double equals operator (==).
This is a valid construct by itself - an if statement does not necessarily need an else statement to go with it. If we left the application like this, the code in the if statement would only execute when a user passed an argument into the application. When a user didn't pass an argument into the application, nothing would be printed to the console, but the program wouldn't crash.
To add an else statement to an if construct, we can use the else keyword immediately after the if statement's code block. The else statement doesn't need a condition because the code in this code block is executed when the condition for the if statement evaluates to false. Inside this else code block, I will add the code to print a generic greeting to the Console.
Run the application again and verify that it no longer throws an unhandled exception and that the output is as expected. You can see the output of the application in the Debug Console.
We should also check that the application still works correctly when the user passes in an argument. You could test this without the debugger in the Command Prompt window, but you can also configure the debugger to pass arguments to the application when it launches.
To do this, open the launch.json file, under the.vscode subfolder.
This file tells Visual Studio Code what to do when it runs an application. There is a lot of information here, but the most important part for us is the args section. In this section, we can specify what arguments to pass to the application when it is launched from Visual Studio Code.
I have added the name Ken, saved the file, and launched the application again to check that the application is printing a personalized greeting as expected. Note that the value in the args array has to be a string, wrapped in double-quotes.
In this part of the series, we learned how to find and fix bugs in our C# .NET projects. We saw how to figure out what exceptions are being thrown in our code and where the problem is using the Console messages. We also saw how to use the debugger in Visual Studio Code to inspect the environment while our application is executing to figure out what exactly is going wrong in our code. Along the way, we learned how to use conditional statements to branch the flow of execution and avoid exceptional situations.
Before moving on, I would encourage you to spend some time playing with the debugger. Step through the whole application line by line, add multiple breakpoints, and figure out the difference between Step Over and Step Into. Debugging exceptions is a normal and important part of programming in C# so you should be comfortable and competent with the tools that are used.
Sign up to the mailing list to get a new post about industrial automation and controls engineering delivered to your inbox every week.
Learn the skills you need to start your journey as a PLC programmer. Enroll in PLC Bootcamp to learn how to write and test your first PLC program for free.