Skip to content

Fix Task<TResult> debug proxy Result behavior and rename from Future#123752

Merged
stephentoub merged 4 commits intomainfrom
copilot/fix-debug-proxy-result-issue
Feb 12, 2026
Merged

Fix Task<TResult> debug proxy Result behavior and rename from Future#123752
stephentoub merged 4 commits intomainfrom
copilot/fix-debug-proxy-result-issue

Conversation

Copy link
Contributor

Copilot AI commented Jan 29, 2026

Description

Task<TResult>'s debug proxy was showing default(TResult) for the Result property of faulted/cancelled tasks, misleading developers about actual behavior. The debugger should show that accessing Result will throw.

Changes

  • Rename SystemThreadingTasks_FutureDebugView<TResult>SystemThreadingTasks_TaskOfTResultDebugView<TResult> ("Future" was an old name for "Task")
  • Change Result property to delegate to m_task.Result when the task is completed, guarded by IsCompleted to avoid blocking the debugger on incomplete tasks
  • Add tests covering the debug proxy behavior for completed, faulted, canceled, and incomplete tasks

Before: public TResult? Result => m_task.Status == TaskStatus.RanToCompletion ? m_task.Result : default;

After: public TResult Result => m_task.IsCompleted ? m_task.Result : default!;

This correctly handles all three task states:

  • Completed successfully: Returns the actual result
  • Faulted/Cancelled: Throws the exception via m_task.Result (the original issue's goal)
  • Still running: Returns default without blocking the debugger

Tests Added

Added 6 tests in TaskDebugViewTests.cs validating debug proxy behavior:

  • Completed successfully → returns the result (reference and value types)
  • Faulted → Result throws AggregateException wrapping the original exception
  • Canceled → Result throws AggregateException
  • Incomplete → Result returns default without blocking (reference and value types)
  • Status/Exception properties correctly reflect faulted state
Original prompt

This section details on the original issue you should resolve

<issue_title>Task<T> debug proxy is misleading about Result of a non-success task</issue_title>
<issue_description>### Description

System.Threading.Task<T> has a custom debug proxy. This debug proxy returns default value of T instead of indicating a potential exception for a Result property of a faulted task

Reproduction Steps

var mt = M();
await ((Task)mt).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

Console.WriteLine(mt.Result);

async Task<string> M()
{
    throw new NotImplementedException();
}
  • Break point on Console... line.
  • Start debugging
  • Reach the break point and inspect Result property

Expected behavior

Debug proxy indicates that accessing Result will throw an exception

Actual behavior

Result shows default value (in this case null), misleading developers about actual property behavior:

Image

Regression?

No

Known Workarounds

No response

Configuration

No response

Other information

No response</issue_description>

<agent_instructions>Change SystemThreadingTasks_FutureDebugView's Result to just delegate to the task's Result. Also rename that type; "Future" was an old name for "Task", so this should be more like "TaskTResultDebugView" or something similar. (If there are other references to "Future" where it should be "Task", please address those, too.)</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@stephentoub > Debug proxy indicates that accessing Result will throw an exception > It shows that the task is in a Faulted state and that there's an exception. Wait, Result, etc will all throw in that state.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix misleading debug proxy for Task<T> Result property Fix Task&lt;TResult&gt; debug proxy Result behavior and rename from Future Jan 29, 2026
Copilot AI requested a review from stephentoub January 29, 2026 13:30
@stephentoub stephentoub marked this pull request as ready for review January 29, 2026 13:31
Copilot AI review requested due to automatic review settings January 29, 2026 13:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #123752

🎯 Holistic Assessment

Motivation: Address a debugger UX issue where \Task\ debug proxy showed \default(TResult)\ for faulted/cancelled tasks, misleading developers into thinking \Result\ would return that value rather than throw.

Approach:

  1. Rename \SystemThreadingTasks_FutureDebugView\ → \SystemThreadingTasks_TaskOfTResultDebugView\ to modernize naming
  2. Change \Result\ property to delegate directly to \m_task.Result\ instead of returning \default\ for non-success states

Net Positive: ✅ Yes - Simple, targeted fix that improves debugger accuracy.


📋 Detailed Findings

✅ Correctness

  • The rename from "Future" to "TaskOfTResult" is appropriate - "Future" was an internal code name for \Task\ from early development
  • Delegating \Result\ directly to \m_task.Result\ correctly shows the exception that would be thrown for faulted/cancelled tasks in the debugger
  • The change is consistent with the non-generic \SystemThreadingTasks_TaskDebugView\ naming pattern in \Task.cs\

✅ Consistency

  • Only one file contains references to this type, so the rename is complete
  • The new naming follows the established pattern from the base \Task\ class's debug view (\SystemThreadingTasks_TaskDebugView)

💡 Minor Observation

  • The removal of ?\ from \TResult?\ to \TResult\ is correct since the property now directly returns the task's result (which throws for non-success) rather than potentially returning \default\ which could be null

🧪 Test Quality

This is a debugger metadata change. The behavior is verified by debugging experience rather than unit tests, which is appropriate for this type of change.


✅ Summary

Verdict: Approve

This is a clean, minimal fix that:

  1. Modernizes legacy naming ("Future" → "TaskOfTResult")
  2. Correctly exposes the throwing behavior in the debugger for faulted tasks
  3. Follows established patterns in the codebase

The change is surgical (4 lines) and addresses exactly what was requested in the issue.

@stephentoub
Copy link
Member

🤖 Copilot Code Review — PR #123752

Holistic Assessment

Motivation: The PR correctly identifies a real usability issue: the debug proxy was displaying default(TResult) for faulted/canceled tasks, which is misleading since accessing Task<TResult>.Result in those states throws an exception. The renaming from "Future" to "TaskOfTResult" is also appropriate modernization.

Approach: The approach is incomplete. While delegating directly to m_task.Result correctly exposes the exception-throwing behavior for faulted/canceled tasks, it fails to guard against incomplete (still running) tasks, where Task<TResult>.Result blocks indefinitely.

Summary: ⚠️ Needs Changes. The change introduces a critical blocking risk in the debugger for tasks that haven't completed yet. This must be fixed before merge.


Detailed Findings

❌ Blocking Risk for Incomplete Tasks — Must fix

The new implementation public TResult Result => m_task.Result; directly calls Task<TResult>.Result, which blocks the calling thread when the task is not yet completed:

// From Future.cs lines 449-452
internal TResult GetResultCore(bool waitCompletionNotification)
{
    // If the result has not been calculated yet, wait for it.
    if (!IsCompleted) InternalWait(Timeout.Infinite, default);
    ...
}

When a debugger evaluates properties on the debug proxy, this will cause the debugger to hang indefinitely for any task that is still WaitingToRun or Running. This is worse than the original behavior.

Suggested fix: Guard the access to only invoke m_task.Result when the task is already completed:

public TResult Result => m_task.IsCompleted ? m_task.Result : default!;

This achieves the PR's goal (showing exceptions for faulted/canceled tasks) while avoiding blocking for incomplete tasks.


✅ Naming — Correct

The rename from SystemThreadingTasks_FutureDebugView<TResult> to SystemThreadingTasks_TaskOfTResultDebugView<TResult> is consistent with the existing SystemThreadingTasks_TaskDebugView for the non-generic Task class.


✅ DebuggerTypeProxy Attribute — Correct

The [DebuggerTypeProxy] attribute on line 57 correctly references the renamed type.


💡 Nullability of Result Property — Suggestion

The return type changed from TResult? to TResult. For the suggested fix above, default! is appropriate because:

  1. For completed-successfully tasks, the actual result is returned
  2. For faulted/canceled tasks, an exception is thrown (so the return value is never used)
  3. For incomplete tasks, default is returned (same as before)

The null-forgiving operator (!) is needed because default for a reference type TResult is null, but the non-nullable return type is intentional to match Task<TResult>.Result.


💡 continuationFuture Variable Names — Out of scope

The instructions mentioned renaming "Future" references, but continuationFuture is a local variable name describing the continuation task—this is idiomatic naming and doesn't need to change.


Notes

This review was synthesized from multiple model analyses (Claude Sonnet, Gemini Pro, GPT-5.1-Codex). The blocking risk was flagged by all models.

@stephentoub
Copy link
Member

@copilot please address the "Must fix" issue called out in the comment

Copy link
Contributor Author

Copilot AI commented Feb 11, 2026

@copilot please address the "Must fix" issue called out in the comment

Fixed in bb008c2. Changed Result to m_task.IsCompleted ? m_task.Result : default! so it avoids blocking on incomplete tasks while still properly throwing for faulted/cancelled ones.

Copilot AI changed the title Fix Task&lt;TResult&gt; debug proxy Result behavior and rename from Future Fix Task<TResult> debug proxy Result behavior and rename from Future Feb 11, 2026
Copilot AI requested a review from stephentoub February 11, 2026 20:03
@tarekgh
Copy link
Member

tarekgh commented Feb 12, 2026

@copilot can you add new tests covering this area and validating the change?

Copy link
Contributor Author

Copilot AI commented Feb 12, 2026

@copilot can you add new tests covering this area and validating the change?

Added 6 tests in bbfc1dc covering the debug proxy behavior:

  • Completed successfully → returns the result (reference and value types)
  • Faulted → Result throws AggregateException
  • Canceled → Result throws AggregateException
  • Incomplete → Result returns default without blocking (reference and value types)
  • Status/Exception properties correctly reflect faulted state

Copilot AI requested a review from tarekgh February 12, 2026 16:24
Copy link
Member

@tarekgh tarekgh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub the changes LGTM. Some tests have been added to cover this area, maybe worth taking a quick look before merging. Thanks!

Copilot AI and others added 4 commits February 12, 2026 13:20
…askOfTResultDebugView and delegate Result to task.Result

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
…complete tasks

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task<T> debug proxy is misleading about Result of a non-success task

4 participants