2
\$\begingroup\$

I repost my question here because it has been closed on stack overflow

This question could sound stupid but I am new to the logging, displaying and handling exceptions. Because before I was just doing a big try catch (Exception ex) to get exceptions (test softwares)

But now I am doing a "real" software, which will have a public release. And I am asking what is the right way of doing those things.

I am using WPF with the .NET Framework 4.7.2, and i'm also using NLog

For now, I did a little code, and I am just asking like a review / advices. I put that code at startup, so in the app.xaml.cs with a protected override OnStartup()

using System;
using System.IO;
using System.Windows;
using WPFTemplate.Classes;
using WPFTemplate.Classes.Core;

namespace WPFTemplate {

    public partial class App : Application {

        /// <summary>
        /// Logger
        /// </summary>
        private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();

        /// <summary>
        /// Application starting point
        /// </summary>
        protected override void OnStartup(StartupEventArgs e) {

            // ---- Application

            // -- Directory
            try {

                // Create application main directory
                Directory.CreateDirectory(Reference.AppPath);

            } catch (Exception ex) {

                // Log
                Log.Error(ex);

                // The path isn't valid/found (ex: with network name)
                if (ex is DirectoryNotFoundException || ex is IOException || ex is NotSupportedException) {
                    MessageBox.Show(string.Format("The application path '{0}' isn't valid..\nPlease check if the path is valid", Reference.AppPath), Reference.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
                }
                // The user hasn't the permission to write (then need to run the application as administrator)
                else if (ex is UnauthorizedAccessException) {
                    MessageBox.Show(string.Format("The application hasn't the right to write data in the application path '{0}'..\nPlease start the application as administrator", Reference.AppPath), Reference.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
                }
                // Path is too long (max: 256 chars)
                else if (ex is PathTooLongException) {
                    MessageBox.Show(string.Format("The application path '{0}' is too long..\nPlease install the application in another place", Reference.AppPath), Reference.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
                }

                // Shutdown
                Application.Current.Shutdown();

            }

        }

        protected override void OnExit(ExitEventArgs e) {

            // -- Logs

            Log.Info("Stopping the application");

            NLog.LogManager.Shutdown();

        }

    }

}

I did a custom message for every exceptions, and it's a little bit long

So i'm waiting for your reviews/advices and thanks a lot i'm still a beginner in that x)

\$\endgroup\$
  • \$\begingroup\$ In C# a Try/Catch block will be withing a function, and the function will be within a class. To get any review you need to provide the entire function, and to get a good review you should provide the entire class. This question is currently off-topic because there is not enough code presented. \$\endgroup\$ – pacmaninbw Jan 8 at 17:58
  • 1
    \$\begingroup\$ I edited the code \$\endgroup\$ – NaolShow Jan 8 at 18:15
  • \$\begingroup\$ If you want to catch all Exceptions, you should take a look here stackoverflow.com/a/1472562/4961688 or here stackoverflow.com/a/46804709/4961688 \$\endgroup\$ – Tim Pohlmann Jan 9 at 11:25
  • \$\begingroup\$ Avoid coding in the code-behind and instead implement MVVM when working with WPF. Here you've written 30/40 lines merely to configure the location of the log files; imagine how complicated things will become when you need to implement actual business logic in your app. \$\endgroup\$ – BCdotWEB Jan 10 at 14:23
4
\$\begingroup\$

The main reason for exceptions (from a developer point of view) is to detect malfunctioning code. Because once this malfunctioning code is executed, the result of an operation and therefore the state of an application can be considered as undefined, since unexpected/unpredictable side effects will transition the application to an unpredictable state.

Data might be in an unpredictable state i.e. data integrity is not guaranteed any more. Unmanaged resources might be initialized or reserved but not released, which may even affect other applications. So most of the time you want the application to stop execution to avoid unpredictable behavior.

Exceptions are designed to hold a program. They are controlled on OS level so that Operating System can stop the application and also provide a snapshot from the stack memory, that was occupied by the faulting application.

We can say that there are expected errors e.g., not enough disk space to save a file or wrong user credentials and there are unexpected errors e.g. a null reference exception. So if the application could recover/rollback from an error, this error will be an expected error. You know in advance what can fail and you know how to handle this situation to keep the application operating reliably. You preferably would want to handle such errors "silently", without throwing exceptions, as exceptions have impact on application performance and therefore user experience.
You also don't want the exception to propagate through your complete application, which is expensive in terms of performance. Therefore you would like to handle it directly where the exception occurred. This implies that you better don't handle exceptions in a big catch block around your application. You always catch exceptions where they occur. This is a best practice for the reasons explained.

It's also best practice to use multiple catch blocks and catch specific exceptions only. This way your code becomes more readable. Specific exceptions are those and only those you can handle. And logging is not considered handling (if this exception is not relevant you should have avoided it in the first place). Every other exception must be thrown as they are unexpected or have heavy impact on the stability (predictability) of the application.

This means catching Exception, like it is happening in your code, is a very bad idea:

try 
{
  // Create application main directory
  Directory.CreateDirectory(Reference.AppPath);
} 
catch (Exception ex) // Bad: this will swallow every exception although unhandled by this catch block
{
  Log.Error(ex);

  if (ex is DirectoryNotFoundException || ex is IOException || ex is NotSupportedException) 
  {
    MessageBox.Show("Error");
  }
}

Bad exception handling as all exceptions are swallowed. Logging an exception is not handling an exception.

What is happening here, is that all exceptions are swallowed. Especially those that are critical to the application. By catching Exception you basically handle every exception. Catching Exception at application level makes it even worse: a NullReferenceException will never see the light. A IndexOutOfRangeException will never get attention too, as you didn't even re-throw anything (re-throwing an exception is also generally considered as bad practice). A NullReferenceException must crash your application and requires the developer's full attention. If Reference.AppPath throws a NullReferenceException because Reference or AppPath is null we will never notice it without consulting any log file.

In real life, nobody is reading log files. Every developer avoids it. And for sure, nobody reads them daily to check if any of the hundreds of customers experienced a critical runtime error. Indeed, it's an uncomfortable situation when a customer experiences an application crash, but at least it will draw everybody's attention to look for an immediate fix. An application error reporter can be a very powerful enhancement to provide quick and proper bug support.
Nevertheless log files can be a very valuable support to reproduce and find bugs.

Also, exceptions of the same type might get thrown at different code segments in a totally different context. A global exception handler will have a hard time to get the context of the exception to find out if NotSupportedException was thrown in context of a invalid user input (recoverable error) or a wrong call to an API method (non-recoverable).

To solve the issue of swallowing exceptions, you have to get rid of the smelly switch statement, which already indicates that you are doing something "wrong". Type checking in a switch statement (or ugly and unreadable if-else constructs) is a classic code smell.

The goal is to handle exceptions that are non-critical i.e. the application can recover from and leave every other exception unhandled:

try 
{
  // Create application main directory
  Directory.CreateDirectory(Reference.AppPath);
} 
catch (DirectoryNotFoundException ex) 
{
  // Log
  Log.Error(ex);

  MessageBox.Show("Error");
}
catch (UnauthorizedAccessException ex) 
{
  // Log
  Log.Error(ex);

  MessageBox.Show("Error");
}

Now, any developer that comes across this code knows immediately what is going on here. The code has become more readable and the ugly and smelly switch was successfully eliminated. But more important, every unhandled exception will still crash the app, to indicate a critical bug. If Reference.AppPath throws a NullReferenceException we will now notice it first hand this time, without the need of consulting any log file.

Don't catch what you can't handle.

If you wish to log unhandled exceptions, instead subscribe to the Application.DispatcherUnhandled event:

public partial class App : Application
{
  public App()
  {
    this.DispatcherUnhandledException += (sender, args) => this.logger.Error(args.Exception.Message);
  }
}

Free Resources

As a note: it's best practice to take care of managed/unmanaged resources and use finalizers i.e. finally or using blocks, to make sure that these resources are released i.e. disposed properly under any circumstances before letting the application crash.

In case the following code throws an exception, the using statement will execute a finalizer before the forced exit:

using (var sqlConnection = new SqlConnection(args))
{
  sqlConnection.ExecuteQuery(queryArgs);
}

This is equal to:

try
{
  var sqlConnection = new SqlConnection(args);
  sqlConnection.ExecuteQuery(queryArgs);
}
finally
{
  sqlConnection.Dispose();
}

Both implementations won't handle the critical exception, but will release unmanaged resources before the method is put on hold and the exception routing is executed by the OS.

Re-throwing Exceptions

Although re-throwing exceptions is considered bad practice, it makes sense if you need to add data to the exception or like to throw a more specialized maybe custom exception type. But make sure to keep the stack trace intact.

Bad re-throw as it will break the stack trace:

try 
{
  // Create application main directory
  Directory.CreateDirectory(Reference.AppPath);
} 
catch (DirectoryNotFoundException directoryNotFoundException) 
{
  Log.Error(directoryNotFoundException);

  // The previous stack trace of 'directoryNotFoundException' will be lost
  throw new UnknownUserInputException("Custom Message");
}

Good re-throw as it will preserve the full stack trace by setting
the original exception as inner exception of the wrapping exception type:

try 
{
  // Create application main directory
  Directory.CreateDirectory(Reference.AppPath);
} 
catch (DirectoryNotFoundException directoryNotFoundException) 
{
  Log.Error(directoryNotFoundException);

  // Use constructor to set inner exception
  throw new UnknownUserInputException("Custom Message", directoryNotFoundException);
}

Avoiding Exceptions in the First Place

Microsoft best practices recommends to "Design classes so that exceptions can be avoided".
This is especially true for user input, since this are always errors we can recover from. In general, data input whether the source is a user or a client application, can be validated before it enters the application. The goal is to have the application to deal with valid data only. The logic should not process invalid data and throw an exception. The data persistence layers should never persist invalid data.

To prevent invalid data from being processed, you would typically use a form of data validation. In case of a WPF application, which is what you are implementing, I highly recommend to apply the MVVM pattern and implement the INotifyDataErrorInfo interface (example) which is part of the WPF framework and enables error propagation to the UI to provide a visual feedback. If the user enters an invalid file path you can easily make the UI draw e.g. a red border around the concerning input field, with messages or hints, forcing the user to correct the input.

In this special scenario of picking a file or folder, the best way is to use a file or folder picker dialog. This way, invalid filesystem path inputs are almost impossible.

Another example is file saving. Before saving a file, you can check whether the operation can be executed by checking the available disk space to avoid I/O exceptions.

Almost all expected exceptions can be avoided by checking the preconditions before executing a critical operation.


The bottom line is, that you should never catch Exception. Swallowing exceptions is dangerous as it will conceal serious bugs. To log an exception is not handling an exception. Logging an exception and done is equal to swallowing it, as you can't be sure somebody will ever read the log data or search it for random errors.
If you wish to log all unhandled exceptions by default, subscribe to the Application.DispatcherUnhandledException event, but never use a global catch block at application level.
Especially in your case where the errors or exceptions are triggered by invalid user input, you are advised to use data validation. This makes exception handling obsolete and is the preferred way. User input validation e.g., by implementing INotifyDataErrorInfo, includes a convenient way to give the user feedback and prompt him to correct the input.
In case the exception is not critical, in a away that it requires the application to halt, consider to roll it back to a defined state before continuing execution in order to prevent dirty data or undefined states.
Don't forget to free resources using a finally or using block to properly close/dispose those objects.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you for your response! It really helps me a lot! I will follow your advice ;) Thanks again and have a nice evening! \$\endgroup\$ – NaolShow Feb 7 at 17:09
0
\$\begingroup\$

Remove logs from Starup.cs and add the logging into every method.

You can override the onException method, where you can write your own customized message and exception

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.