That's because Dictionary<TKey, TValue> is implementing both the generic version of IDictionary<TKey, TValue> and the non-generic version (just IDictionary that yields an Object).
Looking at the source, you can easily see that the indexer of the generic version explicitly throws when the key is not found:
public TValue this[TKey key]
{
get
{
int i = FindEntry(key);
if (i >= 0) return entries[i].value;
ThrowHelper.ThrowKeyNotFoundException(); // <-- this line
return default(TValue);
}
}
But the non-generic version does not:
object IDictionary.this[object key]
{
get
{
if (IsCompatibleKey(key))
{
int i = FindEntry((TKey)key);
if (i >= 0)
return entries[i].value;
}
return null;
}
}
Then, unless you explicitly cast your Dictionary to the non-generic version, you're calling the indexer that throws when the key is not found.
Update (as per your comment):
Since IDictionary (the non-generic version) was introduced in .NET 1.0 (when there was no support for generics yet) the only type it consumed and returned is obviously System.Object (which can be tested for null).
Hence, it made sense not to throw when key is not found because one could simply do this:
dict["nonExistingKey"] != null
But, once the generic IDictionary<TKey, TValue> was introduced, the TValue can also be set to a value type (like int or DateTime) and the above pattern would have to be adjusted to:
var dict = new Dictionary<string, int>();
dict["nonExistingKey"] != 0;
dict["nonExistingKey"] != default(int);
Which is less fluent, less standard and more cumbersome to use.
From "design perspective", I presume they decided to preserve backward compatibility for IDictionary while proactively throwing for the generic version.