When can a null check throw a NullReferenceException

Viewed 2865

I know this might seem impossible at first and it seemed that way to me at first as well, but recently I have seen exactly this kind of code throw a NullReferenceException, so it is definitely possible.

Unfortunately, there are pretty much no results on Google that explain when code like foo == null can throw a NRE, which can make it difficult to debug and understand why it happened. So in the interest of documenting the possible ways this seemingly bizarre occurrence could happen.

In what ways can this code foo == null throw a NullReferenceException?

5 Answers

in C# you can overload operators to add custom logic on some comparison like this. For example:

class Test
{
    public string SomeProp { get; set; }
    
    public static bool operator ==(Test test1, Test test2)
    {
        return test1.SomeProp == test2.SomeProp;
    }

    public static bool operator !=(Test test1, Test test2)
    {
        return !(test1 == test2);
    }
}

then this would produce a null reference exception:

Test test1 = null;
bool x = test1 == null;

One example is with getters:

class Program
{
    static void Main(string[] args)
    {
        new Example().Test();
    }
}

class Example
{
    private object foo
    {
        get => throw new NullReferenceException();
    }

    public void Test()
    {
        Console.WriteLine(foo == null);
    }
}

This code will produce a NullReferenceException.

While quite esoteric, it is possible to cause this type of behavior via custom implementations of DynamicMetaObject. This would be a rare but interesting example of where this could occur:

void Main()
{
    dynamic foo = new TestDynamicMetaObjectProvider();
    object foo2 = 0;
    
    Console.WriteLine(foo == foo2);
}

public class TestDynamicMetaObjectProvider : IDynamicMetaObjectProvider
{
    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new TestMetaObject(parameter, BindingRestrictions.Empty, this);
    }
}

public class TestMetaObject : DynamicMetaObject
{
    public TestMetaObject(Expression expression, BindingRestrictions restrictions)
        : base(expression, restrictions)
    {
    }

    public TestMetaObject(Expression expression, BindingRestrictions restrictions, object value)
        : base(expression, restrictions, value)
    {
    }

    public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg)
    {
        // note it doesn't have to be an explicit throw.  Any improper property
        // access could bubble a NullReferenceException depending on the 
        // custom implementation.
        throw new NullReferenceException();
    }
}

Not literally your code, but awaiting a null task will also throw:

public class Program
{
    public static async Task Main()
    {
        var s = ReadStringAsync();
        if (await s == null)
        {
            Console.WriteLine("s is null");
        }
    }

    // instead of Task.FromResult<string>(null);
    private static Task<string> ReadStringAsync() => null;
}

Do note however that the debugger can get the location of throwing statements wrong. It might show the exception thrown at the equality check, while it occurs at earlier code.

foo == null does indeed to operator overload resolution, and the operator in question didn't handle being passed a null. We are starting to consider writing foo == null obsolete and preferring (taking a page from Visual Basic) foo is null or !(foo is null) which is soon to be full is not null to explicitly inline a null pointer check.

Fix your operator== implemenation. It shouldn't throw, but it is.

Related