views:

321

answers:

8

Hi,

I work on a database application written in C# with sql server as backend. And for data integrity, I try to enforce as much as possible on database level - relations, check constraints, triggers.

Due to them, if data is not consistent, the save / update / insert can fail, and app throw SqlException.

I do various validations both in UI (to present user with meaningful info if data entered is not valid), also in BL, which reports errors back to UI which presents it to user.

However, there are things that really cannot be checked in the app, and are handled by the db: I mean errors on delete when no cascade delete and user try to delete a entity from a master table, etc.

E.g. Employees table acts as master in lot of relations - manager of employee, manager of department, cashier, team leader, teams members etc, etc. If I add anew employee which is not involved in any relation I can delete it, but of user try to delete one that is master oin such relation, the delete fails (as it should) due to RI rules enforced at DB level, and that's ok.

I write delete code in a try ... catch and handle the exception, telling user he cannot delete that employee. But I want to give user more meaningful info - the reason the record cannot be deleted. Maybe it was just a test employee record, which was also added to a test team. But user forget where added that and if I could tell "Cannot delete employee because it is part of team T1", user will know to go first to Team T1, remove user then try to delete it again. That's a simple example, since as I said an employee can be involved in lot of relations - in my app I have at least 20.

The solution is to display the Message reported by SqlException, but that's not elegant at all. First, that msg is very technical - it talks about FK, PK, Triggers, which are meaningless for users and will scare them. Second my app is uses multi-lang UI and all menus and msgs are shown in user selected language (selected either at login time or in user profile). And the msg from SqlException is english (if I use english version) or worst, less common languages, like german or dutch, if it happens that sql server is in that language.

Is there any common or recommended approach to extract meaningful info from sql exception to be able to present user a meaningful msg (e.g. what relation or child table caused the failure, or what trigger, etc). but something I can test in program in a lang-independent fashion and then format my own error msg in a user-friendly way?

How do you handle this situation?

Thanks for all answers

(PS: Sorry for the long post)

+5  A: 

Unfortunately, there isn't an easy answer here.

The amount of work involved will depend on the consistency of your error messages coming from your business layer. You are going to need to do some form of translation from the "technical" error message to your user oriented message.

This should be a matter of making some forms of lookups from your error messages to a resource key, which can be used to pull out your language-specific error message. However, if you need to parse the messages for more information (ie: table names, etc), then it gets a bit trickier. In that case, you'll probably need to have something that maps an error message to a regex/processor of some form as well as a new resource string. Then you could format the user's string with the information you extract from the original error, and present it to the user.

Reed Copsey
I agree - you need an application specific error mapper that will transform sql error messages into business error messages.
Dylan
+2  A: 

The way to do this is to write a stored procedure, and use TRY and CATCH. Use RAISERROR to raise your own custom messages, and check the error code in the SqlException.

John Saunders
This would mean changing the business layer, though, which bzamfir seemed to be trying to avoid (it's not always an option).
Reed Copsey
Unfortunately, the information he's looking for won't be there unless it gets put there by a SP.
John Saunders
+1  A: 

Well, from the database, you'll only ever get these technical messages, e.g. "violation of foreign key relation FK_something_to_another" or such.

Typically, in the SqlException, you also get a SQL error code or something else.

The best approach would probably be to have a separate table in your database which basically maps those technical SQL errors that can happen to more meaningful, user-oriented messages. E.g. if your SQL error says something like "fk violation blablabaal", you could have an entry in your "UserErrorTable" which maps this to a user message saying "could not delete user (this.and.that), most likely because ..... (he's still member of a team)" or whatever.

You could then try to catch those SqlExceptions in your business layer, translate those technical infos into a custom exception for your users, put in a user-friendly message, and stick the technical exception into the .InnerException of your custom exception type:

public class UserFriendlyException : Exception
{
  public string UserErrorMessage { get; set; }

  public UserFriendlyException(string message, SqlException exc) : base(message, exc)
  {
     UserErrorMessage = MapTechnicalExecptionToUserMessage(exc);
  }
}

Marc

marc_s
+1  A: 

We usually write some sort of translator in our projects.We compare the SQL exception message with some predefine patterns and show the equivalent message to user

Beatles1692
+1  A: 

About generic technical > userfriendly errors i can only support the answers already giving.

But to your specific example with the employer i must encourage you to not rely only on the SqlException. Before trying to delete the Employee you should make some check to see if he/she is a part of any teams, is manager etc. It will tremendiosuly improve the usability of your application.

Pseudo:

Employee e;
try {

   IEnumerable<Team> teams = Team.GetTeamsByEmployee(e);
   if (teams.Count() > 0) {
       throw new Exception("Employee is a part of team "+ String.Join(",", teams.Select(o => o.Name).ToArray());
   }

   IEnumerable<Employee> managingEmployees = Employee.GetEmployeesImManaging(e);
   if (managingEmployees.Count() > 0) {
       throw new Exception("Employee is manager for the following employees "+ String.Join(",", managingEmployees.Select(o => o.Name).ToArray());
   }

   Employee.Delete(e);
} catch (Exception e) {
   // write out e
}
BurningIce
+1  A: 

Errors happen. When it doesn't particularly matter what kind or type of error you hit, or how you handle it, slapping your code in a TRY...CATCH... block and write a generic error reporting system is fine. When you want (or are required to) write something better than that, it can take serious effort, as outlined in some prior posts (which I too have upvoted).

I tend to classify errors as anticipatable or unanticipatable. If you can anticipate an error and you want to handle it with a clear an consice message, such as with your "delete employee" situation, you will have to plan and code accordingly. By their definition, you cannot do this for unanticipatable errors--that's usually where TRY/CATCH comes in.

For your situation, one way could be before deleting the row, check via queries against the child tables whether or not the delete could succeed. If it won't, you'll know precisely why (and for all child tables, not just the first one that would block the delete), and can present the user with a clear message. Alas, you must consider whether the data can change between the check and the actual delete -- perhaps not a problem here, but it could be in other situations.

An alternative based on the TRY...CATCH... would be to check the error number within the catch block. If it's a "can't delete due to foreign key", you could then query the children tables and generate the message, and if it was some other unanticipated error, you'd have to fall back on a generic message.

(A caveat: some times when it hits an error, SQL will raise two error messages in a row [is the FK constraint violation one of them?] and in these situations the various ERROR() functions will only return data for the second and invariably less useful message. This is incredibly aggravating, but there's not too much you can do about it.)

Philip Kelley
+1  A: 

The short answer is "don't." Let the errors bubble out to the global error handling/logging. The validation and business rules should generally preclude database exceptions, so you are better off failing hard and fast and not submitting dirty data. Transactions help too.

Wyatt Barnett
+2  A: 

Error Messages do not equate Exceptions. An error message is something that the user should find informative and most important actionable. There are some guidelines around error messages in the User Experience Guidelines I recommend you read up. Apple also has good generic guidelines in Writing Good Alert Messages.

You'll immediately notice that most, if not all, of SQL errors are not good end user messages. 'Constraint FKXF#455 violation' - Bad error for end user. 'Filegroup is full' - Bad error for end user. 'Deadlock' - same. What good applications do is they separate the roles of users. Administrators need to see these errors, not end users. So the application always logs the full SQL error with all the details, eventually notifies the administrator, and then display a different error to the user, something like 'A system error occurred, the administrator was notified'.

If the SQL error can is actionable by the end user, then you can display him an error message instruct him what to do to fix the problem (eg. change the invoice date in the input to satisfy a constraint). But even in this case most times you should not display the SQL error straight to the user ( you already seen a very good reason why not: localization). I understand that this creates a much bigger workload for your development team. You must know what errors are user actionable in each case, from the multitude of errors you may catch. That is well known, and that's exactly why good program managers know that about 80% of the code is handling the error cases, and why the application being 'done' usually means is 20% done. This is what differentiates great apps from ordinary ones: how they behave when things go wrong.

My suggestion is to start up with the principle of progressive disclosure. Display an generic error message stating 'the operation has failed'. Offer to display more details if the user presses a 'Show details...' button on the error message dialog, and display the SqlError collection (btw, you should always log and display the entire SqlError collection of SqlException.Errors, not the SqlException). Use the SqlError.Number property to add logic in the catch block that decides if the user can do anything about the error (decide if the error is actionable) and add appropriate information.

Unfortunately there is no pixie dust. You are touching the foot hils of what is probably the most difficult part of your project.

Remus Rusanu