Catch multiple exceptions at once?

前端 未结 27 1839
夕颜
夕颜 2020-11-22 11:31

It is discouraged to simply catch System.Exception. Instead, only the "known" exceptions should be caught.

Now, this sometimes leads to unnecce

相关标签:
27条回答
  • 2020-11-22 12:07

    Update 2015-12-15: See https://stackoverflow.com/a/22864936/1718702 for C#6. It's a cleaner and now standard in the language.

    Geared for people that want a more elegant solution to catch once and filter exceptions, I use an extension method as demonstrated below.

    I already had this extension in my library, originally written for other purposes, but it worked just perfectly for type checking on exceptions. Plus, imho, it looks cleaner than a bunch of || statements. Also, unlike the accepted answer, I prefer explicit exception handling so ex is ... had undesireable behaviour as derrived classes are assignable to there parent types).

    Usage

    if (ex.GetType().IsAnyOf(
        typeof(FormatException),
        typeof(ArgumentException)))
    {
        // Handle
    }
    else
        throw;
    

    IsAnyOf.cs Extension (See Full Error Handling Example for Dependancies)

    namespace Common.FluentValidation
    {
        public static partial class Validate
        {
            /// <summary>
            /// Validates the passed in parameter matches at least one of the passed in comparisons.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="p_parameter">Parameter to validate.</param>
            /// <param name="p_comparisons">Values to compare against.</param>
            /// <returns>True if a match is found.</returns>
            /// <exception cref="ArgumentNullException"></exception>
            public static bool IsAnyOf<T>(this T p_parameter, params T[] p_comparisons)
            {
                // Validate
                p_parameter
                    .CannotBeNull("p_parameter");
                p_comparisons
                    .CannotBeNullOrEmpty("p_comparisons");
    
                // Test for any match
                foreach (var item in p_comparisons)
                    if (p_parameter.Equals(item))
                        return true;
    
                // Return no matches found
                return false;
            }
        }
    }
    

    Full Error Handling Example (Copy-Paste to new Console app)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Common.FluentValidation;
    
    namespace IsAnyOfExceptionHandlerSample
    {
        class Program
        {
            static void Main(string[] args)
            {
                // High Level Error Handler (Log and Crash App)
                try
                {
                    Foo();
                }
                catch (OutOfMemoryException ex)
                {
                    Console.WriteLine("FATAL ERROR! System Crashing. " + ex.Message);
                    Console.ReadKey();
                }
            }
    
            static void Foo()
            {
                // Init
                List<Action<string>> TestActions = new List<Action<string>>()
                {
                    (key) => { throw new FormatException(); },
                    (key) => { throw new ArgumentException(); },
                    (key) => { throw new KeyNotFoundException();},
                    (key) => { throw new OutOfMemoryException(); },
                };
    
                // Run
                foreach (var FooAction in TestActions)
                {
                    // Mid-Level Error Handler (Appends Data for Log)
                    try
                    {
                        // Init
                        var SomeKeyPassedToFoo = "FooParam";
    
                        // Low-Level Handler (Handle/Log and Keep going)
                        try
                        {
                            FooAction(SomeKeyPassedToFoo);
                        }
                        catch (Exception ex)
                        {
                            if (ex.GetType().IsAnyOf(
                                typeof(FormatException),
                                typeof(ArgumentException)))
                            {
                                // Handle
                                Console.WriteLine("ex was {0}", ex.GetType().Name);
                                Console.ReadKey();
                            }
                            else
                            {
                                // Add some Debug info
                                ex.Data.Add("SomeKeyPassedToFoo", SomeKeyPassedToFoo.ToString());
                                throw;
                            }
                        }
                    }
                    catch (KeyNotFoundException ex)
                    {
                        // Handle differently
                        Console.WriteLine(ex.Message);
    
                        int Count = 0;
                        if (!Validate.IsAnyNull(ex, ex.Data, ex.Data.Keys))
                            foreach (var Key in ex.Data.Keys)
                                Console.WriteLine(
                                    "[{0}][\"{1}\" = {2}]",
                                    Count, Key, ex.Data[Key]);
    
                        Console.ReadKey();
                    }
                }
            }
        }
    }
    
    namespace Common.FluentValidation
    {
        public static partial class Validate
        {
            /// <summary>
            /// Validates the passed in parameter matches at least one of the passed in comparisons.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="p_parameter">Parameter to validate.</param>
            /// <param name="p_comparisons">Values to compare against.</param>
            /// <returns>True if a match is found.</returns>
            /// <exception cref="ArgumentNullException"></exception>
            public static bool IsAnyOf<T>(this T p_parameter, params T[] p_comparisons)
            {
                // Validate
                p_parameter
                    .CannotBeNull("p_parameter");
                p_comparisons
                    .CannotBeNullOrEmpty("p_comparisons");
    
                // Test for any match
                foreach (var item in p_comparisons)
                    if (p_parameter.Equals(item))
                        return true;
    
                // Return no matches found
                return false;
            }
    
            /// <summary>
            /// Validates if any passed in parameter is equal to null.
            /// </summary>
            /// <param name="p_parameters">Parameters to test for Null.</param>
            /// <returns>True if one or more parameters are null.</returns>
            public static bool IsAnyNull(params object[] p_parameters)
            {
                p_parameters
                    .CannotBeNullOrEmpty("p_parameters");
    
                foreach (var item in p_parameters)
                    if (item == null)
                        return true;
    
                return false;
            }
        }
    }
    
    namespace Common.FluentValidation
    {
        public static partial class Validate
        {
            /// <summary>
            /// Validates the passed in parameter is not null, throwing a detailed exception message if the test fails.
            /// </summary>
            /// <param name="p_parameter">Parameter to validate.</param>
            /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
            /// <exception cref="ArgumentNullException"></exception>
            public static void CannotBeNull(this object p_parameter, string p_name)
            {
                if (p_parameter == null)
                    throw
                        new
                            ArgumentNullException(
                            string.Format("Parameter \"{0}\" cannot be null.",
                            p_name), default(Exception));
            }
        }
    }
    
    namespace Common.FluentValidation
    {
        public static partial class Validate
        {
            /// <summary>
            /// Validates the passed in parameter is not null or an empty collection, throwing a detailed exception message if the test fails.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="p_parameter">Parameter to validate.</param>
            /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
            /// <exception cref="ArgumentNullException"></exception>
            /// <exception cref="ArgumentOutOfRangeException"></exception>
            public static void CannotBeNullOrEmpty<T>(this ICollection<T> p_parameter, string p_name)
            {
                if (p_parameter == null)
                    throw new ArgumentNullException("Collection cannot be null.\r\nParameter_Name: " + p_name, default(Exception));
    
                if (p_parameter.Count <= 0)
                    throw new ArgumentOutOfRangeException("Collection cannot be empty.\r\nParameter_Name: " + p_name, default(Exception));
            }
    
            /// <summary>
            /// Validates the passed in parameter is not null or empty, throwing a detailed exception message if the test fails.
            /// </summary>
            /// <param name="p_parameter">Parameter to validate.</param>
            /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
            /// <exception cref="ArgumentException"></exception>
            public static void CannotBeNullOrEmpty(this string p_parameter, string p_name)
            {
                if (string.IsNullOrEmpty(p_parameter))
                    throw new ArgumentException("String cannot be null or empty.\r\nParameter_Name: " + p_name, default(Exception));
            }
        }
    }
    

    Two Sample NUnit Unit Tests

    Matching behaviour for Exception types is exact (ie. A child IS NOT a match for any of its parent types).

    using System;
    using System.Collections.Generic;
    using Common.FluentValidation;
    using NUnit.Framework;
    
    namespace UnitTests.Common.Fluent_Validations
    {
        [TestFixture]
        public class IsAnyOf_Tests
        {
            [Test, ExpectedException(typeof(ArgumentNullException))]
            public void IsAnyOf_ArgumentNullException_ShouldNotMatch_ArgumentException_Test()
            {
                Action TestMethod = () => { throw new ArgumentNullException(); };
    
                try
                {
                    TestMethod();
                }
                catch (Exception ex)
                {
                    if (ex.GetType().IsAnyOf(
                        typeof(ArgumentException), /*Note: ArgumentNullException derrived from ArgumentException*/
                        typeof(FormatException),
                        typeof(KeyNotFoundException)))
                    {
                        // Handle expected Exceptions
                        return;
                    }
    
                    //else throw original
                    throw;
                }
            }
    
            [Test, ExpectedException(typeof(OutOfMemoryException))]
            public void IsAnyOf_OutOfMemoryException_ShouldMatch_OutOfMemoryException_Test()
            {
                Action TestMethod = () => { throw new OutOfMemoryException(); };
    
                try
                {
                    TestMethod();
                }
                catch (Exception ex)
                {
                    if (ex.GetType().IsAnyOf(
                        typeof(OutOfMemoryException),
                        typeof(StackOverflowException)))
                        throw;
    
                    /*else... Handle other exception types, typically by logging to file*/
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 12:08

    How about

    try
    {
        WebId = Guid.Empty;
        WebId = new Guid(queryString["web"]);
    }
    catch (FormatException)
    {
    }
    catch (OverflowException)
    {
    }
    
    0 讨论(0)
  • 2020-11-22 12:09

    Since I felt like these answers just touched the surface, I attempted to dig a bit deeper.

    So what we would really want to do is something that doesn't compile, say:

    // Won't compile... damn
    public static void Main()
    {
        try
        {
            throw new ArgumentOutOfRangeException();
        }
        catch (ArgumentOutOfRangeException)
        catch (IndexOutOfRangeException) 
        {
            // ... handle
        }
    

    The reason we want this is because we don't want the exception handler to catch things that we need later on in the process. Sure, we can catch an Exception and check with an 'if' what to do, but let's be honest, we don't really want that. (FxCop, debugger issues, uglyness)

    So why won't this code compile - and how can we hack it in such a way that it will?

    If we look at the code, what we really would like to do is forward the call. However, according to the MS Partition II, IL exception handler blocks won't work like this, which in this case makes sense because that would imply that the 'exception' object can have different types.

    Or to write it in code, we ask the compiler to do something like this (well it's not entirely correct, but it's the closest possible thing I guess):

    // Won't compile... damn
    try
    {
        throw new ArgumentOutOfRangeException();
    }
    catch (ArgumentOutOfRangeException e) {
        goto theOtherHandler;
    }
    catch (IndexOutOfRangeException e) {
    theOtherHandler:
        Console.WriteLine("Handle!");
    }
    

    The reason that this won't compile is quite obvious: what type and value would the '$exception' object have (which are here stored in the variables 'e')? The way we want the compiler to handle this is to note that the common base type of both exceptions is 'Exception', use that for a variable to contain both exceptions, and then handle only the two exceptions that are caught. The way this is implemented in IL is as 'filter', which is available in VB.Net.

    To make it work in C#, we need a temporary variable with the correct 'Exception' base type. To control the flow of the code, we can add some branches. Here goes:

        Exception ex;
        try
        {
            throw new ArgumentException(); // for demo purposes; won't be caught.
            goto noCatch;
        }
        catch (ArgumentOutOfRangeException e) {
            ex = e;
        }
        catch (IndexOutOfRangeException e) {
            ex = e;
        }
    
        Console.WriteLine("Handle the exception 'ex' here :-)");
        // throw ex ?
    
    noCatch:
        Console.WriteLine("We're done with the exception handling.");
    

    The obvious disadvantages for this are that we cannot re-throw properly, and -well let's be honest- that it's quite the ugly solution. The uglyness can be fixed a bit by performing branch elimination, which makes the solution slightly better:

    Exception ex = null;
    try
    {
        throw new ArgumentException();
    }
    catch (ArgumentOutOfRangeException e)
    {
        ex = e;
    }
    catch (IndexOutOfRangeException e)
    {
        ex = e;
    }
    if (ex != null)
    {
        Console.WriteLine("Handle the exception here :-)");
    }
    

    That leaves just the 're-throw'. For this to work, we need to be able to perform the handling inside the 'catch' block - and the only way to make this work is by an catching 'Exception' object.

    At this point, we can add a separate function that handles the different types of Exceptions using overload resolution, or to handle the Exception. Both have disadvantages. To start, here's the way to do it with a helper function:

    private static bool Handle(Exception e)
    {
        Console.WriteLine("Handle the exception here :-)");
        return true; // false will re-throw;
    }
    
    public static void Main()
    {
        try
        {
            throw new OutOfMemoryException();
        }
        catch (ArgumentException e)
        {
            if (!Handle(e)) { throw; }
        }
        catch (IndexOutOfRangeException e)
        {
            if (!Handle(e)) { throw; }
        }
    
        Console.WriteLine("We're done with the exception handling.");
    

    And the other solution is to catch the Exception object and handle it accordingly. The most literal translation for this, based on the context above is this:

    try
    {
        throw new ArgumentException();
    }
    catch (Exception e)
    {
        Exception ex = (Exception)(e as ArgumentException) ?? (e as IndexOutOfRangeException);
        if (ex != null)
        {
            Console.WriteLine("Handle the exception here :-)");
            // throw ?
        }
        else 
        {
            throw;
        }
    }
    

    So to conclude:

    • If we don't want to re-throw, we might consider catching the right exceptions, and storing them in a temporary.
    • If the handler is simple, and we want to re-use code, the best solution is probably to introduce a helper function.
    • If we want to re-throw, we have no choice but to put the code in a 'Exception' catch handler, which will break FxCop and your debugger's uncaught exceptions.
    0 讨论(0)
  • 2020-11-22 12:09

    Note that I did find one way to do it, but this looks more like material for The Daily WTF:

    catch (Exception ex)
    {
        switch (ex.GetType().Name)
        {
            case "System.FormatException":
            case "System.OverflowException":
                WebId = Guid.Empty;
                break;
            default:
                throw;
        }
    }
    
    0 讨论(0)
  • 2020-11-22 12:09

    I want to suggest shortest answer (one more functional style):

            Catch<FormatException, OverflowException>(() =>
                {
                    WebId = new Guid(queryString["web"]);
                },
                exception =>
                {
                    WebId = Guid.Empty;
                });
    

    For this you need to create several "Catch" method overloads, similar to System.Action:

        [DebuggerNonUserCode]
        public static void Catch<TException1, TException2>(Action tryBlock,
            Action<Exception> catchBlock)
        {
            CatchMany(tryBlock, catchBlock, typeof(TException1), typeof(TException2));
        }
    
        [DebuggerNonUserCode]
        public static void Catch<TException1, TException2, TException3>(Action tryBlock,
            Action<Exception> catchBlock)
        {
            CatchMany(tryBlock, catchBlock, typeof(TException1), typeof(TException2), typeof(TException3));
        }
    

    and so on as many as you wish. But you need to do it once and you can use it in all your projects (or, if you created a nuget package we could use it too).

    And CatchMany implementation:

        [DebuggerNonUserCode]
        public static void CatchMany(Action tryBlock, Action<Exception> catchBlock,
            params Type[] exceptionTypes)
        {
            try
            {
                tryBlock();
            }
            catch (Exception exception)
            {
                if (exceptionTypes.Contains(exception.GetType())) catchBlock(exception);
                else throw;
            }
        }
    

    p.s. I haven't put null checks for code simplicity, consider to add parameter validations.

    p.s.2 If you want to return a value from the catch, it's necessary to do same Catch methods, but with returns and Func instead of Action in parameters.

    0 讨论(0)
  • 2020-11-22 12:11

    As others have pointed out, you can have an if statement inside your catch block to determine what is going on. C#6 supports Exception Filters, so the following will work:

    try { … }
    catch (Exception e) when (MyFilter(e))
    {
        …
    }
    

    The MyFilter method could then look something like this:

    private bool MyFilter(Exception e)
    {
      return e is ArgumentNullException || e is FormatException;
    }
    

    Alternatively, this can be all done inline (the right hand side of the when statement just has to be a boolean expression).

    try { … }
    catch (Exception e) when (e is ArgumentNullException || e is FormatException)
    {
        …
    }
    

    This is different from using an if statement from within the catch block, using exception filters will not unwind the stack.

    You can download Visual Studio 2015 to check this out.

    If you want to continue using Visual Studio 2013, you can install the following nuget package:

    Install-Package Microsoft.Net.Compilers

    At time of writing, this will include support for C# 6.

    Referencing this package will cause the project to be built using the specific version of the C# and Visual Basic compilers contained in the package, as opposed to any system installed version.

    0 讨论(0)
提交回复
热议问题