Throwing exceptions is in itself trivial. Just invoke throw
with an exception object. But when should you throw? Should you create a new exception or rethrow an old one? What should you include in the exception information?
- What are exceptions?
- Designing exceptions
- Throwing exceptions
In the previous two articles, you’ve got to know what exceptions are and how to design them. This time I’ll go through some typical scenarios and show you how to throw exceptions.
As I’ve written earlier, exceptions should be thrown when something exceptional has happened. That is, if you expect something to be a certain way, it’s exceptional if it isn’t.
Validating Arguments
The most simple example is when you expect method arguments:
public void PrintName(string name)
{
Console.WriteLine(name);
}
In this case, you EXPECT that a correct name is supplied, as it’s the only way to ensure that the method works as expected. The code above will work with <c>null
as an argument, but all you’ll see is an empty line. The problem is that you won’t detect that something went wrong (other than the empty line). The worst thing is that those kind of errors are hard to detect as from the code’s perspective it seems to work, while from the users perspective they do not get the expected output.
Now imagine that error in a production system where the business logic (“BL-A”) comes to an incorrect conclusion thanks to invalid arguments. That in turn will probably surface somewhere else in your application. For instance, where the generated result is used in some other business logic (“BL-B”). If you are really unlucky, your users will notice that “BL-B” doesn’t work as expected in some cases. That will lead to several hours of debugging (and probably adding a lot of logging) before discovering that it’s actually “BL-A” that didn’t get the expected arguments.
I would at any time choose to spend some extra time on making sure that I get reasonable values into my method arguments instead of having to fix business logic bugs later.
Let’s add validation:
public void PrintName(string name)
{
if (name == null) throw new ArgumentNullException("name");
Console.WriteLine(name);
}
Now we get an exception directly instead of deeper down in the business logic. That check might be trivial, but without it, we would probably get the dreaded NullReferenceException
somewhere deeper down in the call tack.
In this case, the name should also contain a specific length, and may only contain alpha numerics:
public void PrintName(string name)
{
if (name == null) throw new ArgumentNullException("name");
if (name.Length < 5 || name.Length > 10)
throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long");
if (name.Any(x => !char.IsAlphaNumeric(x))
throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics");
Console.WriteLine(name);
}
If you find yourself repeating that code, you could create either a Validator
method on the Person
class or create a PersonName
class and do the validation in it. It depends on your application type and how serious you think the invalid argument is.
I personally do sanity checks in all of my methods, but only do business validations in those methods that do business logic.
You Failed the Code
The other time to throw exceptions is when a method can’t deliver the expected result. A controversial example is a method with a signature like this:
public User GetUser(int id)
{
}
It’s quite common that methods like that return <c>null
when the user is not found. But is it really the correct way of doing so? I would say NO. I got three reasons to that.
First, the method is called GetUser()
and not TryGetUser()
or FindUser()
. We are saying that we SHOULD get an user. Not delivering a user is therefore an exceptional case.
Second, we have somewhere got an ID for an user. And if that ID is not found in the data source, something is probably wrong.
Third, in most scenarios, we do expect to find an user. Hence it’s an exceptional case if we do not find one. By using null
as a return value, we do communicate that we expect the user to be not found and not that it’s something exceptional.
The null Cancer
By using null
, we have to have this code everywhere:
var user = datasource.GetUser(userId);
if (user == null)
throw new InvalidOperationException("Failed to find user: " + userId);
That kind of code spreads like cancer when you allow null
as a return value.
Why don’t we just use:
public User GetUser(int id)
{
if (id <= 0) throw new ArgumentOutOfRangeException
("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?");
var user = db.Execute<User>("WHERE Id = ?", id);
if (user == null)
throw new EntityNotFoundException("Failed to find user with id " + id);
return user;
}
Now we get more DRY code since we don’t have to make sure that we get a user back. We can instead rely on the global exception handling to display the error message to the user.
Simply think twice by deciding that returning null
is OK. In most cases, an exception is to prefer.
Context Information
If you take the example with GetUser()
, you’ll see that the EntityNotFoundException
is quite worthless without getting the ID. If you want to switch to better exception handling (i.e., not being afraid of throwing exceptions in all exceptional cases), you’ll also have to make sure that the exception messages are meaningful.
“Meaningful” means that you with the help of the exception message can prevent the exception in the future. That means that the future bug fix should be made in the method that provides the invalid user ID and not the GetUser()
, as the latter would be a workaround and not a bug fix.
Rethrowing Exceptions
To me, rethrowing exceptions is just a useful technique to add additional context information:
public void Query(string sql)
{
if (sql == null) throw new ArgumentNullException("sql");
if (!sql.Contains("SELECT")) throw new FormatException
("Expected SQL to be a SELECT query. You specified: " + sql);
try
{
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = sql;
using (var reader = cmd.ExecuteReader())
{
return ToList(reader);
}
}
}
catch (DataException err)
{
throw new QueryException(sql, err);
}
}
If the ADO.NET exceptions would have had enough information, rethrowing would have not been necessary.
If you catch one exception and throw another one, always make sure to add the original exception as the inner one as shown in the above example.
Catch/Log/rethrow
Some use catch/log/rethrow.
public void Query(string sql)
{
if (sql == null) throw new ArgumentNullException("sql");
if (!sql.Contains("SELECT")) throw new FormatException("Expected SQL to be a SELECT query.
You specified: " + sql);
try
{
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = sql;
using (var reader = cmd.ExecuteReader())
{
return ToList(reader);
}
}
}
catch (Exception err)
{
_logger.Error("Failed", err);
throw;
}
}
DON’T.
ALL different application types in .NET have places where you can catch unhandled exceptions to log them. Simply only rethrow exceptions if there is a real reason to do so.
Do Not Rethrow Caught Exceptions
The callstack is empty when you create a new exception. It’s generated as the exception travels up the call stack.
That means that every time you throw an exception, the call stack is rebuilt as the exception travels up.
The following code will therefore result in a rebuilt callstack.
try
{
}
catch (Exception err)
{
throw err;
}
If you want to keep the original callstack, you have to either use some hacks or wrap the exception with a new exception. A typical example in the .NET Framework is when you invoke a method using reflection. If an exception occurs, it’s wrapped with the TargetInvocationException
.
Summary
Let’s summarize what we have learned:
- Use the method definition to determine what’s exceptional. If it’s unclear: Is the method/argument(s) naming as good as you think?
- Validate the method contract and always throw if it’s violated.
- Always throw if the method can’t deliver the expected result (try to avoid returning <c>
null
if possible). - Try to include context information in the exceptions when throwing instead of using extensive logging.