This doesn't determine, as far as I can see, when a business constraint
I agree. I think the question of when to throw an exception versus
returning a sentinel value is one of the least understood and most error
prone aspects of .NET/C#.
This is the central issue, and I think that the right way to go is to have
"rich" APIs, to make a clear distinction between "special cases" and
"exceptions" and to deal with special cases through return codes or sentinel
values rather than through EH mechanisms.
The basic idea is to have pairs of entry points like:
int Parse(string); // throws exception if string does not represent an int
bool TryParse(string, ref int); // returns false if string does not
represent an int
FileStream OpenFile(string name) // never returns null, throw exception if
file does not exist
FileStream TryOpenFile(string name) // returns null if file does not exist
(but still throws exception if file exists but cannot be opened)
Then, depending on the context, you call one or the other:
1) If you are in a situation where the "exception" must be dealt with
"locally", i.e. where you would put a try/catch around the call to catch a
FileNotFoundException, then you use the "Try" form and you don't catch any
exception locally.
2) Otherwise, you use the non-Try form and you let the exception bubble up.
If you are in case 1, it means that the exception that you would be catching
is not really an exception, it is a "special case" that you are actually
expecting and upon which you need to react in a special way. For example, if
you are parsing user input, you know in advance that the input may not be
valid, and you know that you have to handle this "special case". So, you
should use TryParse. Also, if you are trying to open a file by looking up up
a list of search paths (if not found in path1, try path2, ..), then, the
fact that the file does not exist is not really an exception, it is
something that is part of your design, and you should use TryOpenFile.
If you are in case 2, it means that the exception is really an exception,
i.e. something caused by an "abnormal" context of execution, and you do not
have any "local" action to take to deal with it. For example, if you are
parsing a stream that has been formatted by another program according to a
well defined protocol that ensures that ints are correctly formatted, you
should use Parse rather than TryParse. Also, if you are trying to open a
vital file that has been setup by your installation program, or if you are
trying to open a file that another method has created just before, you
should use OpenFile rather than TryOpenFile.
Of course, this approach forces you to duplicate some entry points in your
APIs, but it has many advantages:
* You reduce the amount of EH code. You get rid of all the local try/catch,
and you only need a few try/catchall in "strategic" places of your
application, where you are going to log the error, alert the user, and
continue. With this scheme, exceptions are handled in a uniform way (you
don't need to discriminate amongst exception types) and the EH code becomes
very simple (only 2 basic patterns for try/catch) and very robust.
* You clearly separate the "application" logic from the "exception handling"
logic. All the "special cases" that your application must deal with are
handled via "normal" constructs (return values, if/then/else), and all the
"exceptional" cases are handled in a uniform way and go through the
try/catchall constructs that you have put in strategic places. You can
review the application logic without having to analyze complex try/catch
constructs spread throughout the code. You can also more easily review the
EH and verify that all exceptions will be propertly logged and that the user
will know about them if he needs to, without having to go into the details
of the application logic.
* It enforces clear "contracts" on your methods: OpenFile and TryOpenFile do
basically the same thing but they have different contracts and choosing one
or the other "means" something: if you read a piece of code and see a call
to TryOpenFile, you know that there is no guarantee "by design" that the
file will be there; on the other hand, if you see a call to OpenFile, you
know that the file should be there "by design" (it was created by another
method before, or it is a vital file created by the installation program,
etc.). Of couse, the fact that the file should be there "by design" does not
mean that it will always be there, but from your standpoint, the fact that
it would not be there is just as exceptional as it being corrupt or the disk
having bad sectors, and the best thing your program can do in this case is
log the error with as much information as possible and tell the user that
something abnormal happened.
* You will get optimal performance because the exception channel will only
be used in exceptional situations (and the cost of logging the exception
will probably outweight the cost of catching it anyway).
So, when I see "local" EH constructs that catch "specific" exceptions, my
first reaction is: API Problem!
In some cases, the caller is at fault and he should use another API or
perform some additional tests before the call.
In other cases, the callee is at fault because he provided an incomplete API
that does not provide a way to perform this specific test without catching
an exception. In this second case, we have the review the API and enhance it
(unless it is a third party API that we don't control, in which case we
usually introduce a helper method that does the dirty try/catch work and
exposes the "richer" API).
Morale: Don't "program" with exceptions (by catching specific exceptions)
but design good APIs what will let you program without them (by letting the
real exceptions bubble up to a generic catch handler).
Bruno.