ToString Returns Null

ID

csharp.tostring_returns_null

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

api-contract, formatting, null-handling, reliability

Description

Reports return null inside an override of ToString(). Callers rely on a string coming back; the correct way to say "nothing to show" is string.Empty.

Rationale

ToString is the most widely called method in .NET and almost never called explicitly. String interpolation and concatenation invoke it, the formatting APIs invoke it, the collection and LINQ helpers that render a sequence invoke it, and so do the debugger and every logging call that accepts an object. None of those call sites are written with a null result in mind, and none of them are visible when you are editing the override.

The consequences split in two, and both are unpleasant. Where the result is used directly — trimmed, measured, compared — the null surfaces as a null reference exception at a call site that has nothing obviously to do with this type. Where it is composed into larger text, nothing fails at all: the value is treated as empty and the object simply vanishes from the output. That is the worse case. A log line or an error message that was supposed to identify the object now identifies nothing, reads as if it were complete, and offers no trail back to the method responsible.

Returning string.Empty avoids both. It concatenates and interpolates safely everywhere, and a caller that genuinely needs to distinguish "no representation" can test for it explicitly.

The returned expression is followed through the shapes that produce the literal conditionally: any parentheses around it, either branch of a conditional such as text is null ? null : text.Trim(), and the right operand of ??. A null on the left of ?? is what the operator exists to replace, so it is not followed.

Only the parameterless override is reported. An overload like ToString(string format) is an ordinary method of the type rather than an implementation of that contract, so what it returns is the type’s own decision. A return null inside a lambda or local function nested in the override is not reported either, because it is not the method’s result.

public class Tag
{
    private readonly string label;

    public Tag(string label) => this.label = label;

    public override string ToString()
    {
        if (label == null) return null;            // FLAW — callers get a null string
        return label;
    }
}

public class Trimmed
{
    private readonly string cached;

    public Trimmed(string cached) => this.cached = cached;

    public override string ToString() => cached is null ? null : cached.Trim();  // FLAW — one branch is null
}

public class Marker
{
    private readonly string label;

    public Marker(string label) => this.label = label;

    public override string ToString() => label ?? string.Empty;   // OK — always a string
}

Remediation

Return string.Empty where the code currently returns null:

public override string ToString() => label ?? string.Empty;

When the null was standing in for an object with no useful representation, return something that identifies the type instead — nameof or GetType().Name beats an empty string in a log. When it was a guard against a null field, use ?? at the point of use, which keeps the null-handling next to the field it protects.

If a caller really does need to distinguish "no representation" from a genuinely empty one, expose that through a separate member with a nullable return type, and leave ToString honouring its contract.