1. Getting to Know the Call Stack
Last time, we briefly mentioned the call stack, but now let's break it down in more detail.
Imagine this: a user clicks a button in your app. That button triggers the OnClick() method ("Button clicked"). That, in turn, calls LoadData() (loading data), and that one calls ReadFromFile() (reading from a file).
Suddenly, in ReadFromFile(), an error happens — file not found. Who's to blame?
To figure it out, the program goes "backtracking": from ReadFromFile() → to LoadData() → to OnClick(). This path is called the call stack — like a stack of plates, where the last one on top falls off first.
The program goes down this stack until it finds a matching catch, but it also runs all the finally blocks along the way — so everything gets closed, freed, and cleaned up.
In programming, the call stack is a list that remembers who called whom, so if something goes wrong, you can walk back this "request path" to find out what caused the crash.
How It Works
When your program runs, every method (or function) call adds a new "line" to the call stack (the list). If something throws an exception, the .NET Exception class saves info about this stack: where and in what order the methods were called that led to the error.
2. Why Do We Even Need the Call Stack?
The call stack is your best buddy when debugging gnarly errors.
- It shows not just what happened, but also where exactly and who's at fault.
- Sometimes, looking at the stack, you'll be surprised how your program even got into that state (especially if someone accidentally passed the wrong argument value).
Typical story: Let's say you have a huge project, with methods calling each other ten levels deep. Suddenly, you get a NullReferenceException, but you have no clue how the program got there. You open the stack — you see the whole chain of calls, and now it's way easier to know where to start digging.
Example:
class MyClass
{
public void MethodA() { MethodB(); }
public void MethodB() { MethodC(); }
public void MethodC() {
throw new Exception("Error!");
}
public void Main()
{
try
{
MethodA();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
}
}
3. How to Create Your Own Exceptions
Sometimes Built-in Exceptions Aren't Enough
.NET has a ton of built-in exceptions (ArgumentNullException, InvalidOperationException, etc.), but sometimes that's just not enough:
- Your app has its own "rules of the game": like, a user can't buy more than 10 items at once, or your business logic shouldn't allow negative amounts.
- You want to separate app logic errors from "system" errors.
That's when you want to make your "own thing" — well, since you can!
using System;
// Custom exception: user not found
public class UserNotFoundException : Exception
{
// base constructor
public UserNotFoundException() : base("User not found.") { }
// constructor with message
public UserNotFoundException(string message) : base(message) { }
// constructor with message and inner exception
public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
Quick Note on Constructors
- No parameters — sets a default message
- With your own message — sometimes you want to add details
- With an inner exception (inner) — so if an error happened "inside another error", you don't lose important info.
How to Use Your Exception
Let's say we're modeling a user search method in our task management app:
using System;
public class UserService
{
public string FindUserNameById(int userId)
{
// "Searching" for user, if not found — throw exception
if (userId != 42)
throw new UserNotFoundException($"User with id {userId} not found.");
return "Maxim";
}
}
In the main program:
// In Main
var service = new UserService();
try
{
string name = service.FindUserNameById(17);
Console.WriteLine("User name: " + name);
}
catch (UserNotFoundException ex)
{
Console.WriteLine("User search problem: " + ex.Message);
// Call stack is also available here via ex.StackTrace
}
If we pass an id that's not 42, we get:
User search problem: User with id 17 not found.
4. Reasons to Make Your Own Exceptions
Logging and Error Separation
Let's say your app has a bunch of different errors, and you need to handle them differently. For example, database errors get logged as "fatal", user errors — you show a red message to the user, and network errors — you try the operation again. Grouping by exception type is a great way to do this.
OOP and Inheritance
You can organize an error hierarchy for your domain:
public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }
Now, if you catch MyAppException, you'll handle all your domain errors, and if you want a special reaction to "too big an order" — just catch the most specific case.
5. What to Keep in Mind When Making Your Own Exceptions
- Don't throw exceptions just for the heck of it
Only make your own exceptions when:- It really helps make your code clearer
- There's a chance they'll be caught from the calling code
- You want to give more info to the calling code (via fields/properties)
- Good practice: Serialization
In .NET, standard exceptions support serialization (for sending over the network, for example). In simple apps, you rarely need serialization, but for "advanced" cases — add the [Serializable] attribute and implement a constructor for serialization (see the official docs). For learning examples you can skip it, but at work — ask your team lead :)
Serialization is the process of turning an object into a format that's easy to store or send (like to a file or over the network). We'll dig into serialization more later.
6. Call Stack Gotchas: Where Things Can Get Messy
The call stack only shows the path to the point where the exception happened. If you catch an exception and throw a new one without passing the "inner" exception (see the Exception(string, Exception inner) constructor), you might lose the original error source. This is called "stack hiding".
Bad:
try
{
// Some error happens here
}
catch (Exception ex)
{
throw new Exception("An unknown error occurred."); // Previous stack is lost!
}
Good:
try
{
// Some error happens here
}
catch (Exception ex)
{
throw new Exception("An unknown error occurred.", ex); // Keep the original stack!
}
Then StackTrace will keep both the path to the first error and your new message.
7. Practical Tips and Common Mistakes
- Don't throw exceptions to control regular logic (like "breaking out of a loop" — there are way more elegant ways!).
- Catch the right type of exception — always catching Exception isn't always a good idea (you might "swallow" an error you shouldn't have).
- Always log the call stack for tricky errors. It'll save you a ton of nerves when hunting bugs.
- Use inner exceptions — it helps you not lose info about the root cause of a crash.
- Make your exceptions clear — so someone reading the code a year later gets why this error happened.
GO TO FULL VERSION