0 Comments

What happens if an exception is thrown by your LINQ query? Well, if you were iterating through with a foreach statement, then you’ll break out of that iteration and process no further elements. Or if you were calling a LINQ extension method that internally iterates through your IEnumerable sequence, such as Max or ToList, then that operation will fail. You won’t even know what the maximum so far was, or get access to the partially constructed list.

But what if you are doing something that you know might occasionally fail, and you would just like to suppress the problem elements from your sequence. How can you do that?

Suppose I have a list of URLs, and a LINQ statement that downloads the HTML content from each of those URLs, and looks to see if it contains the word “azure”. You can see an example here, where I have a custom method called DownloadAsString that uses a WebClient to perform the download.

var sites = new[] { 
   "http://www.pluralsight.com",
   "http://doesntexist.xyz",
   "http://markheath.net",
   "http://www.linqpad.net",
};

sites
   .Select(url => DownloadAsString(url))
   .Where(html => Regex.Match(html, "azure").Success)
.Dump(); // LINQPad extension method to show the output

Obviously this could fail if one of the sites was temporarily unavailable. How could we allow the LINQ query to carry on regardless and make sure we attempt to download and check all the remaining URLs?

Well one way would be to put a try catch block around our DownloadAsString method. Then, if we failed to download a URL, we could return null. Obviously we’d then need to add in another Where statement to our pipeline, because we need to get rid of those nulls before we try to do a Regex.Match on them. Like this:

sites
.Select(url =>
{
    try
    {
        return DownloadAsString(url);
    }
    catch (Exception e)
    {
        Console.WriteLine($"Error downloading {url}: {e.Message}");
        return null;
    }
})
.Where(html => html != null) // filter out ones that failed to download
.Where(html => Regex.Match(html, "azure").Success)
.Dump();

But this is rather ugly and cumbersome. What we could do with is a custom LINQ extension method called TrySelect that attempts to perform a select action, but catches and suppresses exceptions. There’s quite a few different ways you could build this, depending on exactly which type of exceptions you want to catch, and whether you want to be able to do anything when an exception occurs (like logging it). But here’s a simple example TrySelect method that I created as part of my More Effective LINQ course on Pluralsight.

static class MyExtensions
{
    public static IEnumerable<TResult> TrySelect<TSource, TResult>(this IEnumerable<TSource> source, 
                                                                   Func<TSource, TResult> selector, 
                                                                   Action<Exception> exceptionAction)
    {
        foreach (var s in source)
        {
            TResult result = default(TResult);
            bool success = false;
            try
            {
                result = selector(s);
                success = true;
            }
            catch (Exception ex)
            {
                exceptionAction(ex);
            }
            if (success)
            {
                // n.b. can't yield return inside a try block    
                yield return result;
            }
        }
    }
}

This allows you to pass in an Action<Exception> which allows logging, as well as the selector function which might throw an exception. With this method in place, we can now very simply refactor our error suppressing pipeline to look like this:

sites
.TrySelect(DownloadAsString, ex => Console.WriteLine($"Error downloading: {ex.Message}"))
.Where(html => Regex.Match(html, "azure").Success)
.Dump();

Nice and simple. We could also make a version of this that let you suppress only exceptions of a specific type:

static class MyExtensions
{
    public static IEnumerable<TResult> TrySelect<TSource, TResult, TException>(this IEnumerable<TSource> source, 
                                                                        Func<TSource, TResult> selector, 
                                                                        Action<TSource, TException> exceptionAction)
                                                                        where TException : Exception
    {
        foreach (var s in source)
        {
            TResult result = default(TResult);
            bool success = false;
            try
            {
                result = selector(s);
                success = true;
            }
            catch (TException ex)
            {
                exceptionAction(s,ex);
            }
            if (success)
            {
                // n.b. can't yield return inside a try block    
                yield return result;
            }
        }
    }
}

 

You could take the idea even further and pass an Predicate<Exception> to decide if a particular exception should be caught or not. But hopefully this technique will prove useful to you for those occasions when you need to carry on iterating through a sequence despite exceptions being thrown by individual elements in your sequence.

Want to learn more about LINQ? Be sure to check out my Pluralsight course More Effective LINQ.
Vote on HN
comments powered by Disqus