33

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?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
The Red Fox
  • 810
  • 6
  • 12
  • 14
    Does the static type of `foo` implement the `==` operator? – Joe Sewell Jan 05 '21 at 17:17
  • 1
    @JoeSewell: The class that `foo` is an instance of doesn't explicitly implement the `==` operator, but I would think there should be a default implementation. Either way, I don't intend this question to be about my particular case. I would prefer if it was about how this kind of thing can happen in general. – The Red Fox Jan 05 '21 at 17:24
  • 9
    If you can reproduce the exception under debugger, you can just configure debugger to stop on first chance exception for NullReferenceException. This will allow you to see where the exception is actually thrown (including get-ters, overloaded operators and so on). – Serg Jan 05 '21 at 17:30
  • 5
    If you want to be on the safe side when checking if an instance is null and ignore any operator overrides you can do `foo is null`. This is the same as calling `ReferenceEquals(foo, null);`. – ckuri Jan 05 '21 at 17:31
  • 1
    I think the question should be reopened. It's not supposed to be about my code to help me fix it. In fact, I already fixed my code before asking this question. This question is mainly meant to explore the reasons how something unexpected like this can happen and help other people that might come into this position. So there is no point in me providing more debugging details. That would only stray the question from the intended purpose of it. – The Red Fox Jan 05 '21 at 19:19
  • 1
    I agree that this question shouldn't have been closed. While similar questions may require debugging details, I believe that this one is narrow enough and can serve as a good canonical post, hence, my vote to reopen. – 41686d6564 stands w. Palestine Jan 06 '21 at 02:03
  • 4
    _"This question is mainly meant to explore the reasons..."_ -- Stack Overflow is not the place for "exploring reasons". Such questions are overly broad, lacking focus, and fail to meet the site standards in all kinds of ways. The fact is: you are getting an exception you can't explain, and the only way to explain it is to provide the code that's throwing the exception, **which you haven't done**. ... – Peter Duniho Jan 06 '21 at 03:01
  • 2
    ... The only answer that can be provided is pure speculation; you could get a dozen answers, and yet find none of them actually solve your problem (granted, one of the blind squirrels could find their acorn...but that doesn't justify the question). – Peter Duniho Jan 06 '21 at 03:02
  • 2
    @PeterDuniho the question is narrowly-focused enough. There are at most a handful of reasons and I don't see dozens of answers here. The one taking about properties is the most irrelevant here, as the question is about a variable. – CodeCaster Jan 06 '21 at 09:36
  • 3
    @PeterDuniho: I edited my question to hopefully make my intentions clearer. As I understand it, it should be okay to ask on SO about all the possible ways X could happen, especially when X is such a bizzare and rare thing to happen. Again, I already fixed my own code and it's not about it in any way. It was simply motivated by it and by the lack of any useful links on this topic when I google'd it. I simply want to make it easier for future people to debug and understand why their null check was throwing a NRE. Isn't it very much in the spirit of SO to answer programming questions like this? – The Red Fox Jan 06 '21 at 17:40
  • I would suggest taking a look at the NullReferenceException Class. It lists various reasons you will encounter this exception. https://learn.microsoft.com/en-us/dotnet/api/system.nullreferenceexception?view=net-5.0 – Saad Jan 15 '21 at 00:01

5 Answers5

38

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;
Jonesopolis
  • 25,034
  • 12
  • 68
  • 112
  • 13
    Terminology note: this is *overloading* - you can't *override* operators in C#. – Jon Skeet Jan 05 '21 at 19:00
  • 1
    I'd like to add, IMO it's a bad operator design if you don't check to see if the arguments are `null`, I usually factor in the null-ness into the equivalency check, e.g., in `==` if both are `null` then return true, if one is true but not both, return false. IIRC this is how some classes did it in the .NET reference source too. – jrh Jan 06 '21 at 14:27
  • @jrh just make sure to use `object.ReferenceEquals` or you'll get an infinite loop. – Kirk Woll Jan 07 '21 at 14:30
  • @KirkWoll yep, it's been a while since I've done this, but I think I used something like [this](https://rextester.com/OVF10973) – jrh Jan 07 '21 at 15:30
  • 1
    @KirkWoll: Or better (more concise and more idiomatic these days), use `is null`. – Jon Skeet Jan 24 '21 at 18:24
  • @JonSkeet oh wow, hadn't considered using that for this scenario. Awesome! – Kirk Woll Jan 24 '21 at 20:47
16

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.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
ekke
  • 1,280
  • 7
  • 13
9

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();
    }
}
David L
  • 32,885
  • 8
  • 62
  • 93
7

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.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • "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." I have never heard of that before. Can you explain how or when this can happen and if there is some way to avoid this? – The Red Fox Jan 05 '21 at 18:21
  • 4
    One obvious cause is debugging a release build or using outdated PDBs, but also code with multiple try-catch-throw blocks. See also [Wrong line number on stack trace](https://stackoverflow.com/questions/2493779/wrong-line-number-on-stack-trace) and [Wrong line numbers in stack trace](https://stackoverflow.com/questions/37072185/wrong-line-numbers-in-stack-trace). – CodeCaster Jan 05 '21 at 18:27
  • 1
    Note that the null `Task` is pretty much guaranteed to happen in unit tests that mock interfaces. I.e. you miss matching conditions for setup in your `Moq` and you get the default `null` result for the Task. Reliably puzzles people multiple times. – Alexei Levenkov Jan 06 '21 at 05:41
  • @Alexei Setup() all the things and MockBehavior.Strict all the way, except for loggers. – CodeCaster Jan 06 '21 at 07:53
  • @TheRedFox: I've seen it. Grouping parenthesis follow. Either it's out-of-date PDBs or (the previous line ended by executing a void function call and that function threw the null and (that function was inlined by the jitter or was not considered debuggable code)) or the previous line was a throw statement. – Joshua Jan 06 '21 at 16:25
1

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.

Joshua
  • 40,822
  • 8
  • 72
  • 132
  • 2
    _"Fix your `operator==` implemenation"_ What implementation? The OP didn't mention anything about overloading the `==` operator. Moreover, this possibility is already covered in [Jonesopolis's answer](https://stackoverflow.com/a/65583644/8967612) anyway. – 41686d6564 stands w. Palestine Jan 07 '21 at 20:55