In the previous lessons you've learned to make decisions, repeat actions, and classify values,
but always assuming the data arrives correctly: a number that really is a number, an index
that exists within the array, a book that really is in the catalog. In a real program, that's
not guaranteed: a user might type text where a number is expected, someone might try to lend a
book that doesn't exist, or divide by a value that turns out to be zero. When something like
this happens, C# doesn't just carry on as if nothing happened: it throws an exception, a
special object that interrupts the program's normal flow to signal that something has gone
wrong. This lesson, the last one in Module 2, teaches you to anticipate these situations and
handle them in a controlled way with try/catch/finally, instead of letting the program
stop abruptly.
Content
- What is an exception?
try/catch: catching errorsfinally: code that always runs- Throwing your own exceptions with
throw - Common exception types
- Catching multiple exception types
- Best practices with exceptions
- Full application: lending a book safely
- What is an exception?
An exception is an object (from a class that derives, directly or indirectly, from
System.Exception) representing an error that occurred while the program was running. If
nothing is done to handle it, the exception propagates upward until, if no one catches it, the
program terminates abruptly and prints an error message to the console along with the call
stack (stack trace): the sequence of methods that were executing when the failure
occurred.
int[] availableCopies = { 3, 0, 5 };
Console.WriteLine(availableCopies[10]); // IndexOutOfRangeException: the program stops here
Console.WriteLine("This line never runs");Running this code, the console would show something similar to:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. at Program.Main(String[] args)
The goal of this lesson is to keep predictable errors like this one from stopping BiblioTechConsole entirely, and instead react to them in a controlled way.
try/catch: catching errors
try/catch: catching errorsThe try block wraps code that might fail; the catch block defines what to do if it
actually does fail.
int[] availableCopies = { 3, 0, 5 };
try
{
Console.WriteLine(availableCopies[10]);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Error: attempted to access a position that doesn't exist in the array.");
}
Console.WriteLine("The program keeps running normally.");Unlike the previous example, here the program doesn't stop: the exception is caught, a
readable message is displayed, and execution continues on the line after the try/catch
block.
You can also access the exception object to look up additional information, such as its
Message property:
try
{
string userInput = "not-a-number";
int pages = int.Parse(userInput);
Console.WriteLine($"Pages: {pages}");
}
catch (FormatException ex)
{
Console.WriteLine($"Error converting text to a number: {ex.Message}");
}
finally: code that always runs
finally: code that always runsThe optional finally block contains code that runs always, whether the try block
finishes successfully or an exception is thrown and caught. It's the usual place to release
resources (closing a file, a network connection, or a database connection) that must be closed
no matter what happens.
try
{
Console.WriteLine("Opening the loan log...");
int pages = int.Parse("120");
Console.WriteLine($"Entry processed: {pages} pages");
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
}
finally
{
Console.WriteLine("Closing the loan log.");
}In this example, "Closing the loan log." is always displayed: whether the conversion of
"120" succeeds, or fails and the catch catches the error. You'll see finally play a much
bigger role in Module 5, when working with files and databases, where properly closing open
resources is critical.
- Throwing your own exceptions with
throw
throwBesides catching exceptions thrown by the runtime itself (.NET), your code can throw its own
exceptions with throw, to signal an anomalous situation according to BiblioTech's business
rules.
void RegisterLoan(bool bookAvailable)
{
if (!bookAvailable)
{
throw new InvalidOperationException("Cannot lend a book that isn't available.");
}
Console.WriteLine("Loan registered successfully.");
}
try
{
RegisterLoan(bookAvailable: false);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Invalid operation: {ex.Message}");
}throw new ExceptionType("descriptive message") creates a new instance of the specified
exception and throws it immediately, interrupting the method's normal execution until some
catch block (in this method or in whoever called it) catches it.
- Common exception types
.NET provides many ready-made exception classes for common situations. Using the most specific
type possible (rather than the generic Exception) helps whoever reads the code — and
whoever catches it — understand exactly what went wrong.
| Exception | When it occurs (or is thrown deliberately) |
|---|---|
Exception |
The base class all others derive from; represents "a generic error" |
ArgumentException |
An invalid argument was passed to a method |
ArgumentNullException |
null was passed where a value was expected (derives from ArgumentException) |
InvalidOperationException |
An operation is attempted that isn't valid in the object's/program's current state |
FormatException |
A string doesn't have the expected format for conversion (for example, int.Parse on non-numeric text) |
IndexOutOfRangeException |
An array is accessed at a position outside its bounds |
NullReferenceException |
An attempt is made to use a member (method, property) of a variable that is null |
DivideByZeroException |
An integer division by zero is performed |
void ValidatePageCount(int pages)
{
if (pages <= 0)
{
throw new ArgumentException("The page count must be greater than zero.", nameof(pages));
}
}
try
{
ValidatePageCount(-50);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Invalid argument: {ex.Message}");
}
- Catching multiple exception types
A single try block can have several catch blocks, each for a different exception type. C#
evaluates the catch blocks from top to bottom and runs the first one whose type matches
(or is a base class of) the thrown exception, so the more specific types must always go
before the more generic ones.
string[] bookTitles = { "One Hundred Years of Solitude", "Hopscotch", "Ficciones" };
try
{
Console.Write("Enter the position of the book you want to check: ");
string? input = Console.ReadLine();
int position = int.Parse(input!);
Console.WriteLine(bookTitles[position]);
}
catch (FormatException)
{
Console.WriteLine("You must enter a valid whole number.");
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("There's no book at that position in the catalog.");
}
catch (Exception ex)
{
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}If FormatException and IndexOutOfRangeException came after catch (Exception ex), they
would never run: the C# compiler directly flags that order as an error, because a preceding
catch (Exception ex) block would already catch any exception, making the remaining blocks
unreachable.
It's also possible to catch several types with the same block, using the when operator to
add an extra condition, or simply defining the block for the common base class you're
interested in; for most everyday cases, though, one catch per type (as in the previous
example) is clearer.
- Best practices with exceptions
| Best practice | Why it matters |
|---|---|
| Catch the most specific type possible | Makes it easier to give a precise message and doesn't hide other errors |
| Don't use exceptions for the program's normal flow | They're expensive in performance and make the program's logic harder to follow |
Don't catch generic Exception "just because" |
It can hide real programming errors that should be fixed |
Include descriptive messages in throw new ...(...) |
Helps diagnose the problem without having to guess the cause |
Use finally (or resources that close themselves) to release resources |
Prevents memory leaks, locked files, or open connections |
On the first point in the table — not using exceptions for normal flow — if a situation is
predictable and frequent (for example, checking whether a text can be converted to a number),
it's usually better to use tools like int.TryParse (which you already saw in the variables
lesson of Module 1) rather than wrapping int.Parse in a try/catch and waiting for it to
fail. Exceptions are meant for exceptional situations, not routine validations:
// Preferable for routine validations: no exceptions involved
string input = "abc";
if (int.TryParse(input, out int pages))
{
Console.WriteLine($"Page count: {pages}");
}
else
{
Console.WriteLine("Invalid input.");
}
// Reserve try/catch for what's truly unexpected or beyond your control,
// such as accessing an array at a position that, in theory, should never occur.
- Full application: lending a book safely
Let's close the lesson — and the module — with an example that combines several of the ideas covered: searching for a book by title, validating user input, and throwing and catching your own exceptions when a loan operation isn't possible.
string[] bookTitles = { "One Hundred Years of Solitude", "Hopscotch", "Ficciones" };
bool[] availability = { true, false, true };
void LendBook(string requestedTitle)
{
int foundPosition = -1;
for (int i = 0; i < bookTitles.Length; i++)
{
if (bookTitles[i] == requestedTitle)
{
foundPosition = i;
break;
}
}
if (foundPosition == -1)
{
throw new ArgumentException($"The book '{requestedTitle}' doesn't exist in the catalog.");
}
if (!availability[foundPosition])
{
throw new InvalidOperationException($"The book '{requestedTitle}' is already on loan.");
}
availability[foundPosition] = false;
Console.WriteLine($"Loan of '{requestedTitle}' registered successfully.");
}
string[] requests = { "Hopscotch", "1984", "Ficciones" };
foreach (string request in requests)
{
try
{
LendBook(request);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Could not process the request: {ex.Message}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Could not process the request: {ex.Message}");
}
finally
{
Console.WriteLine($"--- End of processing for '{request}' ---");
}
}Expected output:
Could not process the request: The book 'Hopscotch' is already on loan. --- End of processing for 'Hopscotch' --- Could not process the request: The book '1984' doesn't exist in the catalog. --- End of processing for '1984' --- Loan of 'Ficciones' registered successfully. --- End of processing for 'Ficciones' ---
Notice how the foreach loop keeps processing every request even if one of them fails: thanks
to the try/catch inside the loop itself, an error in one request doesn't prevent the next
ones from being processed, and the finally guarantees that the closing message is displayed
in all three cases.
Common Mistakes and Tips
- Systematically catching generic
Exception: this hides the real error type and makes problems harder to diagnose. Reserve it as a last resort, after the specific types, or to log truly unexpected errors. - Using exceptions for validations that could be anticipated: if you can check the
condition beforehand with an
ifor withTryParse, do so; don't let the program fail "on purpose" just to catch it afterward. - Empty
catchblocks: catching an exception and doing nothing with it (not displaying a message, not logging it) hides errors that should be fixed; at the very least, report the problem. - Wrong order of
catchblocks: more specific types must always come before more generic ones; otherwise, the compiler will flag the generic block as unreachable. - Forgetting to release resources: if you open something that needs to be closed (files,
connections), use
finallyor the constructs that already handle it automatically (you'll see this withusingin Module 5, when working with files). - Uninformative exception messages:
throw new Exception("Error")doesn't help anyone understand what happened; always include relevant details (which book, which value, which operation).
Exercises
-
Write a
try/catchblock that attempts to convert the string"cien"tointwithint.Parse, catches theFormatException, and displays a message stating that the input isn't a valid number. -
Create a local function
void ValidateIsbn(string isbn)that throws anArgumentExceptionwith the message"The ISBN cannot be empty."if theisbnparameter is an empty string (""), and otherwise prints"Valid ISBN."to the console. Call the function with""inside atry/catchthat catches the exception and displays its message. -
Given the array
int[] availableCopies = { 2, 0, 3 };, write atry/catch/finallythat attempts to access position5of the array (which doesn't exist), catches theIndexOutOfRangeExceptiondisplaying a suitable message, and in thefinallydisplays"Lookup finished."regardless of whether there was an error.
Solutions
try
{
int number = int.Parse("cien");
Console.WriteLine(number);
}
catch (FormatException)
{
Console.WriteLine("The input isn't a valid number.");
}
Since "cien" can't be converted to int, a FormatException is thrown, and the catch
block displays the message "The input isn't a valid number."; the program continues
normally after the block.
void ValidateIsbn(string isbn)
{
if (isbn == "")
{
throw new ArgumentException("The ISBN cannot be empty.");
}
Console.WriteLine("Valid ISBN.");
}
try
{
ValidateIsbn("");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
Calling ValidateIsbn("") throws the exception with the given message, which the catch
catches and displays: "The ISBN cannot be empty.".
int[] availableCopies = { 2, 0, 3 };
try
{
Console.WriteLine(availableCopies[5]);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("That position doesn't exist in the array of available copies.");
}
finally
{
Console.WriteLine("Lookup finished.");
}
Attempting to access availableCopies[5] (the array only has positions 0, 1, and 2) throws
an IndexOutOfRangeException; the catch displays the error message and, afterward, the
finally displays "Lookup finished." regardless.
Conclusion
In this lesson you've learned what an exception is and how to catch it with try/catch,
how to guarantee that certain code always runs with finally, how to throw your own
exceptions with throw to flag broken business rules, the most common .NET exception types,
how to catch several types in the right order, and why you shouldn't overuse exceptions for
validations that can be anticipated. BiblioTechConsole can now react to invalid data or
unforeseen situations without grinding to a halt.
This closes Module 2: Control Structures. You now know how to make decisions with
conditionals, repeat actions with loops, classify values with switch, and protect your
program from errors with exceptions — all of it, so far, using loose variables, arrays, and
local functions inside Main. That way of working starts to show its limits as
BiblioTechConsole grows: each book is several loose variables (title, ISBN,
availability...) that have to be kept in sync by hand, and each member is just as many more.
In Module 3: Object-Oriented Programming you'll learn to model these concepts with
classes, grouping data and behavior in a single place: the first step toward
BiblioTechConsole ceasing to be a collection of loose variables and starting to look like a
real, well-organized application.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
