Suppressing Exceptions in LINQ Queries
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.
Comments
Thanks, exactly what I was looking for and nicely explained!
Petr Klíč