Introduction
This is my first contribution to the World Wide Web, so be kind.
In the last couple of years, actual programming became less a challenge than it used to be. My move from VB6 to C#.NET gave some new impulses (I'm still learning the .NET Framework) but my focus shifted more to the technical designs and instruction of other developers.
For the last few months, I am instructing, by chatting and emailing documents, a few developers in India. And believe me, this put the challenge back in the job.
As I already mentioned, this involves quite a few documents. So why not put these documents on The Code Project and see what others have to say about it?
One of the first things I noticed when reviewing source code was the way exceptions were handled. If you take the intended audience (user and/or system administrators) into consideration, this part could do with some improvements. Now let's see if the rest of the world agrees with me.
Note: English is my second language, sorry for any wrong use of words/phrases.
Exceptions
Exceptions interrupt the happy path (http://en.wikipedia.org/wiki/Happy_path) in my application. For me, there are only three kinds of exceptions:
- Technical exceptions
- Business exceptions
- Actual bugs
To distinguish the first two, there is a very simple rule; if exactly the same call (with the same parameters and in the same context) could succeed at another point in time then it is a technical exception, otherwise it is a business exception.
Note: The timeframe we are talking about (for sync process) is generally a few milliseconds. If a server is really busy, then the client would get a (technical) time out exception. A few milliseconds later, the server could have time again to process such a request.
The reasons why we distinguish between these two kinds is:
- Determining cause of problem by a system administrator.
If it is a technical exception, then he should solve it.
If it is a business exception, the user should solve it. - Should a client retry or not?
Especially for a-synchronized processes, this is very useful.
Did you note that I do not blame the developer? I will come to that, do not worry.
Business Exception
In general, this is caused by tripping a (valid?) business rule. For example; storing the information about a user in a database could require a first name. If we did not receive such a first name, we raise a business exception.
If the exact same call is made a second time, we will raise exactly the same exception.
Technical Exception
In general, this has something to do with broken down hardware or unreachable external services. For example; storing the information about a user in a database requires a database. If the database is offline, we raise an exception. If the exact same call is made a second time, and the database is back online, this call would succeed.
Logical Exception
A logical exception is a very special kind of exception. It shows itself as a technical exception (we did not throw it) but resending the same call will always result in the same exception. So in nature, it is a (unhandled) business exception. Examples are:
- Invalid data conversion like converting a
string
to an integer - Changes in external interfaces / incorrect version of external components
- Invalid results of calculations like ‘division by zero’
- Etc.
Generally this is a bug, in the widest sense of the word, and should be fixed by a developer (you did not think I would forget to blame the developers, did you?).
Sample
As an example, an application (in C#) which fetches details of a single user;
public string GetDetailsOfUserAsXml(string userIdentification)
{
Guid UserId = new Guid(userIdentification);
DataSet.userDataTable UserDataSet;
UserDataSet = GetDetailsOfUser(UserId);
string DataToReturn;
string Username = UserDataSet.Rows[0]["name"].ToString();
string UserAge = UserDataSet.Rows[0]["age"].ToString();
DataToReturn = "<data><name>" + Username + "</name>" +
"<age>" + UserAge + "</age></data>";
return DataToReturn;
}
If a user enters a valid and existing GUID, then the method follows the happy path and will return a response like:
<data><name>me</name><age>36</age></data>
A typical novice developer would say “Ok, everything works. Done.”
Wrong Input
No, we are not done yet. Let's consider the situation where the user did not enter a GUID but the name ‘me’.
You would get an ugly logical exception: “GUID should contain 32 digits … “.
This exception was to be expected but does not help this user to solve the problem. So we update the application:
public string GetDetailsOfUserAsXml(string userIdentification)
{
Guid UserId;
try
{
UserId = new Guid(userIdentification);
}
catch (Exception)
{
throw new Exception("Could not convert the received
userIdentification (" + userIdentification + ") to a guid");
}
…
The resulting error is now: Could not convert the received userIdentification
(me) to a guid.
The user of this service now knows that we expected a Guid as userIdentification
instead of the word ‘me’.
Please note that the user in this case is another developer, a normal user does not know the meaning of the word guid, let alone enter a guid as a parameter.
Unexpected Results (1)
We are not done yet. What happens if a user enters a identification which does not exist in the database?
Again an ugly logical exception: “There is no row at position 0“.
This particular exception is not needed, it is a valid ID but there is simply no data to display. Lets fix this ‘bug’;
…
string DataToReturn;
string Username = string.Empty;
string UserAge = string.Empty;
if (UserDataSet.Rows.Count != 0)
{
Username = UserDataSet.Rows[0]["name"].ToString();
UserAge = UserDataSet.Rows[0]["age"].ToString();
}
DataToReturn = "<data><name>" + Username + "</name>" +
"<age>" + UserAge + "</age></data>";
return DataToReturn;
}
The result is now an empty XML:
<data><name/><age/></data>
Unexpected Results (2)
What else could go wrong, you might think. Consider this; the user enters the Guid belonging to ‘someone’ as parameter, but gets the response:
<data><name>me</name><age>36</age></data>
Huh? This is not the expected result, we get the data for ‘me’ and not ‘someone’. The only reason is that the ID is mentioned twice in the database.
Note: This is not exactly a real life example but could happen after an upload at database level.
In this case, the input is valid and we get a valid response. But, in contradiction to the previous chapter, now we have to raise an exception instead of preventing one. Let's also fix this bug:
…
if (UserDataSet.Rows.Count == 1)
{
Username = UserDataSet.Rows[0]["name"].ToString();
UserAge = UserDataSet.Rows[0]["age"].ToString();
}
if (UserDataSet.Rows.Count > 1)
{
throw new Exception("Could not identify a unique user using
userIdentification '" + userIdentification + "', please contact your
system administrator");
}
DataToReturn = "<data><name>" + Username + "</name>" +
"<age>" + UserAge + "</age></data>";
return DataToReturn;
}
The result is now a nice business exception: “Could not identify a unique user using userIdentification
…”.
By the way, do not forget to fire the DBA for making such (non unique IDs) a major mistake.
What is Missing?
Ok, a lot of talking about exceptions, but I did not use a try
-catch
. Why?
Simply because I am not planning to do anything with an exception, so why catch it?
I only catch exceptions if I have to add information to an exception or at the highest level. In the last case, I present the user with a nice message box or place the exception in the eventlog and/or log file.
Please also note that exceptions should not be used to control the flow through the application. Also see the next chapter.
What Not To Do (1)
Exceptions are costly and should not be used to code normal application flow logic. Rather add additional checks. See the example below:
public string TestFileLoopExceptionBased()
{
string Filename = "C:\\Inetpub\\wwwroot\\ExpSample\\App_Data\\TestDoc.txt";
string Result = string.Empty;
long StartTime = DateTime.Now.Ticks;
for (int i = 0; i < 100; i++)
{
try
{
Result = System.IO.File.ReadAllText(Filename);
}
catch (Exception)
{
}
}
Result = (DateTime.Now.Ticks - StartTime).ToString();
return Result;
}
public string TestFileLoop()
{
string Filename = "C:\\Inetpub\\wwwroot\\ExpSample\\App_Data\\TestDoc.txt";
string Result = string.Empty;
long StartTime = DateTime.Now.Ticks;
for (int i = 0; i < 100; i++)
{
if (System.IO.File.Exists(Filename))
{
Result = System.IO.File.ReadAllText(Filename);
}
}
Result = (DateTime.Now.Ticks - StartTime).ToString();
return Result;
}
Results:
TestFileLoop
(valid filename) took 156250 ticksTestFileLoopExceptionBased
(valid filename) also took 156250 ticksTestFileLoop
(invalid filename) again took 156250 ticksTestFileLoopExceptionBased
(invalid filename) took 4531250 ticks
As you can see, the method based on exceptions took just a little bit more time, 29 times more!
Please note that in a real life situation, you should have a combination of both methods. The method TestFileLoop()
does not contain any additional checks or save guards for other exceptions like:
- What happens if the file can't be opened?
- What happens if the length of the file can't be determined?
- What happens if enough memory can't be allocated?
- What happens if the read fails?
- What happens if the file can't be closed?
The purpose for this sample was to show how expensive (in CPU cycles) exceptions, in relation to logical checks, are.
What Not To Do (2)
Consider the following piece of code:
public string GetDetailsOfUserAsXml(string userIdentification)
{
try
{
}
catch (Exception Exp)
{
return Exp.Message;
}
}
Instead of raising an exception, I return the error message (as a valid response!) to the calling party. A small program which could use this web service:
public void btnGetData_Click()
{
string UserDataAsXml;
try
{
UserDataAsXml = WebService.GetDetailsOfUserAsXml(textbox1.Text);
XmlDocument UserData = new XmlDocument();
UserData.LoadXml(UserDataAsXml);
string UserName;
UserName = UserData.SelectSingleNode("data/name").InnerText;
MessageBox.Show("Username is " + UserName);
}
catch (Exception Exp)
{
MessageBox.Show("Unable to display name because " + Exp.Message);
}
}
The developer expects exceptions and, if an exceptions occurs, displays a message informing the user what happened.
Now analyze what happens if an exception occurs in GetDetailsOfUserAsXml
. Let's say the exception returned is "Could not identify a unique user using userIdentification
'x', please contact your system administrator";
The client receives an answer (not an exception!) and tries to parse this as XML. This would result in an actual exception:
System.Xml.XmlException: Data at the root level is invalid. Line 1, position 1
At this point, nobody has an idea what went wrong!
Conclusion
Just a few pointers:
- Catch exceptions if you:
- present them to the user
- if you want to add additional information before re-throwing them
- Add exceptions if the results are different from the expectations for the current method.
GetSingleRecord()
should never return multiple records! - Prevent exceptions if possible.
- Catch exceptions at the last possible point. In general, this is in the event a user started.
- Messages in exceptions are one of the very few points in any application where a user actually reads instructions! So make sure your exceptions have something to tell the user; what went wrong and what to do to prevent/fix it.
- Exceptions are costly, do not code normal application flow based on exceptions. Rather add additional checks.
History
- 28th January, 2009: Initial post