Next up in our continued .NET Exception Handling series we come to the System.Collections.Generic.KeyNotFoundException. In most cases the System.Collections.Generic.KeyNotFoundException
is thrown when attempting to access a Collection
element using a key that doesn't exist within said collection.
In this article we'll take a closer look at the System.Collections.Generic.KeyNotFoundException
, including where it sits in the .NET exception hierarchy, along with some functional C# code examples to help illustrate how System.Collections.Generic.KeyNotFoundExceptions
are typically thrown, as well as how to handle the differences in collection types (and the inability to natively capture the key that caused the exception in the first place). Let's get to it!
System.Exception
base class, or derived from another inherited class therein.System.SystemException
is inherited from the System.Exception
class.System.Collections.Generic.KeyNotFoundException
inherits directly from System.SystemException
.Unfortunately, in spite of the fact that it belongs to the System.Collections
namespace, the System.Collections.Generic.KeyNotFoundException
sometimes has trouble capturing key errors from certain types of collections. For example, accessing an invalid key in List
doesn't throw a System.Collections.Generic.KeyNotFoundException
, whereas doing the same in a Dictionary<TKey, TValue>
does. To help illustrate some of these differences we'll start with the full code sample below, after which we'll go through the various methods and examples in more detail:
using System;
using System.Collections.Generic;
using Utility;namespace Airbrake.Collections.Generic.KeyNotFoundException
{
////// Book interface.
///public interface IBook
{
string Author { get; set; }
string Title { get; set; }
}///
/// Basic Book class.
///public class Book : IBook
{
public string Author { get; set; }
public string Title { get; set; }public Book(string title, string author)
{
Author = author;
Title = title;
}
}internal class Program
{
private static void Main(string[] args)
{
// Create Book Dictionary.
var list = new List
{
new Book("The Stand", "Stephen King"),
new Book("The Name of the Wind", "Patrick Rothfuss"),
new Book("Robinson Crusoe", "Daniel Defoe"),
new Book("The Hobbit", "J.R.R. Tolkien")
};ListExample(list);
Logging.LineSeparator();// Create Book Dictionary.
var dictionary = new Dictionary<int, Book>
{
{ 0, new Book("The Stand", "Stephen King") },
{ 1, new Book("The Name of the Wind", "Patrick Rothfuss") },
{ 2, new Book("Robinson Crusoe", "Daniel Defoe") },
{ 3, new Book("The Hobbit", "J.R.R. Tolkien") }
};DictionaryExample(dictionary);
Logging.LineSeparator();DictionaryUsingTryGetValueExample(dictionary);
Logging.LineSeparator();// Create Book Dictionary.
var improvedDictionary = new ImprovedDictionary<int, Book>
{
{ 0, new Book("The Stand", "Stephen King") },
{ 1, new Book("The Name of the Wind", "Patrick Rothfuss") },
{ 2, new Book("Robinson Crusoe", "Daniel Defoe") },
{ 3, new Book("The Hobbit", "J.R.R. Tolkien") }
};ImprovedDictionaryExample(improvedDictionary);
}private static void ListExample(IReadOnlyList list)
{
try
{
// Output current library.
Logging.Log(list);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
Logging.Log(list[list.Count]);
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}private static void DictionaryExample(IReadOnlyDictionary<int, Book> dictionary)
{
try
{
// Output current library.
Logging.Log(dictionary);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
Logging.Log(dictionary[dictionary.Count]);
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}private static void DictionaryUsingTryGetValueExample(IReadOnlyDictionary<int, Book> dictionary)
{
try
{
// Output current library.
Logging.Log(dictionary);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
if (dictionary.TryGetValue(dictionary.Count, out var book))
{
Logging.Log(book);
}
else
{
Logging.Log($"Element at index [{dictionary.Count}] could not be found.");
}
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}private static void ImprovedDictionaryExample(ImprovedDictionary<int, Book> dictionary)
{
try
{
// Output current library.
Logging.Log(dictionary);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
Logging.Log(dictionary[dictionary.Count]);
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}
}///
/// Improves base Dictionary behavior by including missing key value on failed key retrieval.
///
/// Inherits from Dictionary.
////// Key type.
/// Value type.
public class ImprovedDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public new TValue this[TKey key]
{
get
{
// Try retrieving value by key and assign to value out variable.
if (base.TryGetValue(key, out var value))
{
return value;
}// If failure throw new KeyNotFoundException, including missing key.
throw new System.Collections.Generic.KeyNotFoundException(
$"The given key [{key}] was not present in the dictionary.");
}
set => base[key] = value;
}
}
}using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;namespace Utility
{
////// Houses all logging methods for various debug outputs.
///public static class Logging
{
////// Outputs to if DEBUG mode is enabled,
/// otherwise uses standard .
//////Value to be output to log. public static void Log(string value)
{
#if DEBUG
Debug.WriteLine(value);
#else
Console.WriteLine(value);
#endif
}///
/// When parameter is passed, modifies the output to indicate
/// if was expected, based on passed in `expected` parameter.
/// Outputs the full type and message. //////The to output. ///Boolean indicating if was expected. public static void Log(Exception exception, bool expected = true)
{
string value = $"[{(expected ? "EXPECTED" : "UNEXPECTED")}] {exception.ToString()}: {exception.Message}";
#if DEBUG
Debug.WriteLine(value);
#else
Console.WriteLine(value);
#endif
}///
/// Outputs to if DEBUG mode is enabled,
/// otherwise uses standard .
///
/// ObjectDumper class from .
//////Value to be output to log. public static void Log(object value)
{
#if DEBUG
Debug.WriteLine(ObjectDumper.Dump(value));
#else
Console.WriteLine(ObjectDumper.Dump(value));
#endif
}///
/// Outputs a dashed line separator to
/// if DEBUG mode is enabled, otherwise uses standard .
///
public static void LineSeparator(int length = 40)
{
#if DEBUG
Debug.WriteLine(new string('-', length));
#else
Console.WriteLine(new string('-', length));
#endif
}
}
}
We start with a simple IBook
interface and the Book
class that implements it, which we'll be using throughout our examples to create some data collections:
///
/// Book interface.
///public interface IBook
{
string Author { get; set; }
string Title { get; set; }
}///
/// Basic Book class.
///public class Book : IBook
{
public string Author { get; set; }
public string Title { get; set; }
public Book(string title, string author)
{
Author = author;
Title = title;
}
}
Next we define the ListExample(IReadOnlyList list)
method, which will output the list
before attempting to explicitly output the element at index list.Count
(which, as we know, will exceed the existing indices by one since elements are zero-based):
private static void ListExample(IReadOnlyList list)
{
try
{
// Output current library.
Logging.Log(list);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
Logging.Log(list[list.Count]);
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}
We call the ListExample(IReadOnlyList list)
method in our Program.Main()
method by instantiating a new List
collection to pass in:
// Create Book Dictionary.
var list = new List
{
new Book("The Stand", "Stephen King"),
new Book("The Name of the Wind", "Patrick Rothfuss"),
new Book("Robinson Crusoe", "Daniel Defoe"),
new Book("The Hobbit", "J.R.R. Tolkien")
};
ListExample(list);
As you can probably guess this throws an exception that we see in our log output, however, that thrown exception it isn't a System.Collections.Generic.KeyNotFoundException
:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:46104728)}
Author: "Stephen King"
Title: "The Stand"
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:12289376)}
Author: "Patrick Rothfuss"
Title: "The Name of the Wind"
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:43495525)}
Author: "Daniel Defoe"
Title: "Robinson Crusoe"
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:55915408)}
Author: "J.R.R. Tolkien"
Title: "The Hobbit"
--------------------
[UNEXPECTED] System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
As previously mentioned, unfortunately, using an invalid key within some collection types (such as List<>
) produces a System.ArgumentOutOfRangeException
. Let's try a Dictionary<TKey, TValue>
object instead and see what happens. Here we have the DictionaryExample(IReadOnlyDictionary<int, Book> dictionary)
method that performs the exact same logic as ListExample(IReadOnlyList list)
, except with a passed in Dictionary<int, Book>
instead:
private static void DictionaryExample(IReadOnlyDictionary<int, Book> dictionary)
{
try
{
// Output current library.
Logging.Log(dictionary);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
Logging.Log(dictionary[dictionary.Count]);
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}
To test this method we start by creating a new dictionary instance to pass to the method:
// Create Book Dictionary.
var dictionary = new Dictionary<int, Book>
{
{ 0, new Book("The Stand", "Stephen King") },
{ 1, new Book("The Name of the Wind", "Patrick Rothfuss") },
{ 2, new Book("Robinson Crusoe", "Daniel Defoe") },
{ 3, new Book("The Hobbit", "J.R.R. Tolkien") }
};
DictionaryExample(dictionary);
Executing this code now produces a System.Collections.Generic.KeyNotFoundException
:
Key:
0
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:36849274)}
Author: "Stephen King"
Title: "The Stand"
Key:
1
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:63208015)}
Author: "Patrick Rothfuss"
Title: "The Name of the Wind"
Key:
2
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:32001227)}
Author: "Daniel Defoe"
Title: "Robinson Crusoe"
Key:
3
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:19575591)}
Author: "J.R.R. Tolkien"
Title: "The Hobbit"
--------------------
[EXPECTED] System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
There's one major problem though: The System.Collections.Generic.KeyNotFoundException
class doesn't provide any means of determining which key was the problematic key that wasn't present and caused the exception in the first place. There are a few different ways to deal with this problem:
Dictionary<TKey, TValue>.TryGetValue()
method to explicitly check for a value at the specified key, then handle the boolean
result of that method call to determine what should occur.ImprovedKeyNotFoundException
class that inherits from System.Collections.Generic.KeyNotFoundException
, and includes a .Key
property to track the problematic key. Then create an extension method
for Dictionary<TKey, TValue>
that checks if a passed key
exists in the dictionary. If the key
isn't found, throw a new ImprovedKeyNotFoundException
with the associated problematic .Key
property set for use in the exception output message.ImprovedDictionary<TKey, TValue>
class that inherits from Dictionary<TKey, TValue>
and overrides the TValue
method to perform a TryGetValue()
method call for the provided key. If the key
isn't found, throw a new System.Collections.Generic.KeyNotFoundException
with a modified Message
that includes the problematic key
value.Implementing the first solution of using Dictionary<TKey, TValue>.TryGetValue()
is quite easy, so we'll start with that in the DictionaryUsingTryGetValueExample(IReadOnlyDictionary<int, Book> dictionary)
method:
private static void DictionaryUsingTryGetValueExample(IReadOnlyDictionary<int, Book> dictionary)
{
try
{
// Output current library.
Logging.Log(dictionary);
// Add line seperator for readability.
Logging.LineSeparator();
// Attempt to output element of index equal to count.
if (dictionary.TryGetValue(dictionary.Count, out var book))
{
Logging.Log(book);
}
else
{
Logging.Log($"Element at index [{dictionary.Count}] could not be found.");
}
}
catch (System.Collections.Generic.KeyNotFoundException exception)
{
// Catch KeyNotFoundExceptions.
Logging.Log(exception);
}
catch (Exception exception)
{
// Catch unexpected Exceptions.
Logging.Log(exception, false);
}
}
We're passing the same Dictionary<TKey, TValue>
instance that we used before to call this, so executing the above causes the TryGetValue()
method to fail, outputting our custom failure message that includes the missing key:
Element at index [4] could not be found.
The two remaining options for handling the invalid key
are a bit more complex. However, of the two, the latter option of creating a new ImprovedDictionary<TKey, TValue>
class that inherits from Dictionary<TKey, TValue>
is far less code to implement, so we'll go with that one. To do so we start with the new class:
///
/// Improves base Dictionary behavior by including missing key value on failed key retrieval.
///
/// Inherits from Dictionary.
////// Key type.
/// Value type.
public class ImprovedDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public new TValue this[TKey key]
{
get
{
// Try retrieving value by key and assign to value out variable.
if (base.TryGetValue(key, out var value))
{
return value;
}
// If failure throw new KeyNotFoundException, including missing key.
throw new System.Collections.Generic.KeyNotFoundException(
$"The given key [{key}] was not present in the dictionary.");
}
set => base[key] = value;
}
}
Since this inherits Dictionary<TKey, TValue>
the behavior is nearly identical to a normal dictionary, except for the extra check when retrieving a TValue
. The downside, of course, is remembering to explicitly create dictionary instances using the new ImprovedDictionary<TKey, TValue>
class. To test this out we've created just such an object here:
// Create Book Dictionary.
var improvedDictionary = new ImprovedDictionary<int, Book>
{
{ 0, new Book("The Stand", "Stephen King") },
{ 1, new Book("The Name of the Wind", "Patrick Rothfuss") },
{ 2, new Book("Robinson Crusoe", "Daniel Defoe") },
{ 3, new Book("The Hobbit", "J.R.R. Tolkien") }
};
ImprovedDictionaryExample(improvedDictionary);
The result of our ImprovedDictionaryExample()
method call attempting to access an invalid key
element still throws a System.Collections.Generic.KeyNotFoundException
, but notice that we successfully (and subtly) modified the exception Message
property to include the problematic key:
Key:
0
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:41962596)}
Author: "Stephen King"
Title: "The Stand"
Key:
1
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:42119052)}
Author: "Patrick Rothfuss"
Title: "The Name of the Wind"
Key:
2
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:43527150)}
Author: "Daniel Defoe"
Title: "Robinson Crusoe"
Key:
3
Value:
{Airbrake.Collections.Generic.KeyNotFoundException.Book(HashCode:56200037)}
Author: "J.R.R. Tolkien"
Title: "The Hobbit"
--------------------
[EXPECTED] System.Collections.Generic.KeyNotFoundException: The given key [4] was not present in the dictionary.
To get the most out of your own applications and to fully manage any and all .NET Exceptions, check out the Airbrake .NET Bug Handler, offering real-time alerts and instantaneous insight into what went wrong with your .NET code, along with built-in support for a variety of popular development integrations including: JIRA, GitHub, Bitbucket, and much more.