This is a overview on how proper exception handling transformed my debugging process from guesswork to targeted problem-solving with useful errors in the log.
The Problem: Generic Error Messages
When building an ASP.NET web application for Active Directory password resets, I initially encountered the most frustrating type of error - the generic, unhelpful kind that tells you something went wrong but gives no clue about what or why.
My original error handling looked like this:
try
{
// Reset the password using the application pool identity
WriteToLog(string.Format("Attempting to reset password for user '{0}'",
userPrincipalName));
if (ResetADPassword(userPrincipalName, newPwd))
{
WriteToLog(string.Format("Password reset SUCCESSFUL for user '{0}'",
userPrincipalName));
ShowSuccess("Password successfully reset.");
}
else
{
WriteToLog(string.Format("Password reset FAILED for user '{0}' -
Unable to reset password", userPrincipalName));
ShowError("Failed to reset password. Please contact IT support.");
}
}
catch (Exception ex)
{
WriteToLog(string.Format("ERROR during password reset for user '{0}': {1}",
userPrincipalName, ex.Message));
ShowError("Error: " + ex.Message);
}
The logs would show messages like:
ERROR during password reset for user 'TestUser42': Exception has been thrown by the
target of an invocation.
This told me absolutely nothing useful. The error was happening somewhere in the ADSI calls, but I had no idea what the actual underlying problem was.
The Core Issue: Wrapper Exceptions
The problem was that .NET's DirectoryEntry.Invoke()
method wraps the actual Active Directory errors in a TargetInvocationException
. The real error - the one that would actually help me debug the issue - was buried in the InnerException
property.
My original password reset method looked like this:
private bool ResetADPassword(string userPrincipalName, string newPassword)
{
try
{
// ... user lookup code ...
using (DirectoryEntry userEntry = result.GetDirectoryEntry())
{
// This is where the generic exception was thrown
userEntry.Invoke("SetPassword", new object[] { newPassword });
userEntry.CommitChanges();
return true;
}
}
catch (Exception ex)
{
WriteToLog(string.Format("LDAP password reset failed: {0}", ex.Message));
throw new Exception("Error resetting password: " + ex.Message);
}
}
When the SetPassword
call failed, I would get:
- What I saw: "Exception has been thrown by the target of an invocation"
- What I needed: "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"
The Solution: Unwrapping Exception Layers
I completely rewrote the exception handling to dig deeper into the exception hierarchy and extract the meaningful error information:
private bool ResetADPassword(string userPrincipalName, string newPassword)
{
try
{
// Extract username from UPN if it contains @
string username = userPrincipalName;
if (userPrincipalName.Contains("@"))
{
username = userPrincipalName.Split('@')[0];
}
// ... authentication context setup ...
using (DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath))
{
using (DirectorySearcher searcher = new DirectorySearcher(directoryEntry))
{
searcher.Filter = string.Format("(&(objectCategory=person)
(objectClass=user)(sAMAccountName={0}))", username);
searcher.SearchScope = SearchScope.Subtree;
SearchResult result = searcher.FindOne();
if (result != null)
{
using (DirectoryEntry userEntry = result.GetDirectoryEntry())
{
try
{
// The actual password reset operation
userEntry.Invoke("SetPassword", new object[] { newPassword });
userEntry.CommitChanges();
WriteToLog(string.Format("Password reset successful for user:
{0}", username));
return true;
}
catch (System.Reflection.TargetInvocationException ex)
{
// This is the key change - unwrap the real exception
string innerMessage = ex.InnerException != null ?
ex.InnerException.Message : ex.Message;
WriteToLog(string.Format("SetPassword failed for user {0}: {1}",
username, innerMessage));
// Categorize errors based on the actual underlying issue
if (innerMessage.Contains("password does not meet") ||
innerMessage.Contains("password history") ||
innerMessage.Contains("recently used"))
{
WriteToLog(string.Format("Password policy violation for user
{0}: {1}", username, innerMessage));
throw new Exception("Password violates policy: " +
innerMessage);
}
else if (innerMessage.Contains("access is denied") ||
innerMessage.Contains("insufficient rights"))
{
WriteToLog(string.Format("Access denied setting password
for user {0}: {1}", username, innerMessage));
throw new Exception("Access denied - insufficient
permissions: " + innerMessage);
}
else
{
WriteToLog(string.Format("Unexpected error setting
password for user {0}: {1}", username, innerMessage));
throw new Exception("Password reset failed: " +
innerMessage);
}
}
catch (Exception ex)
{
WriteToLog(string.Format("General error setting password for
user {0}: {1}", username, ex.Message));
throw new Exception("Password reset failed: " + ex.Message);
}
}
}
else
{
WriteToLog(string.Format("User not found: {0}", username));
throw new Exception("User not found: " + username);
}
}
}
}
catch (Exception ex)
{
// Prevent double-wrapping of exceptions
if (ex.Message.StartsWith("Error resetting password:"))
{
throw;
}
else
{
throw new Exception("Error resetting password: " + ex.Message);
}
}
}
Transformation in Error Logs
After implementing the improved exception handling, my log messages transformed from useless to actionable:
Error before update:
2025-06-13 07:28:25 - LDAP password reset failed: Exception has been thrown by the target
of an invocation.
2025-06-13 07:28:25 - ERROR during password reset for user 'TestUser42': Error resetting
password: Exception has been thrown by the target of an invocation.
Error after update:
2025-06-13 07:34:20 - SetPassword failed for user TestUser42: Access is denied.
(Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
2025-06-13 07:34:20 - Access denied setting password for user TestUser42: Access is denied.
(Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
2025-06-13 07:34:20 - ERROR during password reset for user 'TestUser42':
Error resetting password: Access denied - insufficient permissions:
Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
Error Categorization
The improved exception handling also allowed me to provide better feedback to end users by categorizing the types of errors:
// In the main exception handler
catch (Exception ex)
{
WriteToLog(string.Format("ERROR during password reset for user '{0}': {1}",
userPrincipalName, ex.Message));
WriteToLog(string.Format("Stack trace: {0}", ex.StackTrace));
// Provide user-friendly messages based on error type
if (ex.Message.Contains("Password violates policy"))
{
ShowError("Your new password doesn't meet the security requirements.
Please try a different password.");
}
else if (ex.Message.Contains("Access denied"))
{
ShowError("Unable to reset password due to security restrictions.
Please contact IT support.");
}
else if (ex.Message.Contains("User not found"))
{
ShowError("User account not found. Please check your username and try again.");
}
else
{
ShowError("An unexpected error occurred. Please contact IT support. Error: " +
ex.Message);
}
}
HRESULT Preservation
The new approach preserves the Windows error codes (like 0x80070005
) that are crucial for diagnosing Active Directory issues.
Stack Trace Logging
For debugging purposes, I ensured stack traces were always logged:
WriteToLog(string.Format("Stack trace: {0}", ex.StackTrace))
Conclusions
The transformation from generic to specific error handling turned hours of guesswork into minutes of targeted troubleshooting. When I saw Access is denied. (Exception from HRESULT: 0x80070005)
, I immediately knew to investigate Active Directory permissions rather than wasting time checking password policies, network connectivity, or application logic.
This approach not only solved my immediate debugging challenge but also made the application much more maintainable and supportable for future issues.