Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
150720 stories
·
33 followers

How .NET handles exceptions internally (and why they're expensive)

1 Share

This blog post is originally published on https://blog.elmah.io/how-net-handles-exceptions-internally-and-why-theyre-expensive/

What really happens when you write throw new Exception() in .NET? Microsoft guidelines state that

When a member throws an exception, its performance can be orders of magnitude slower. 

It's not just a simple jump to a catch block, but a lot goes in CLR (Common Language Runtime). Expensive operations such as stack trace capture, heap allocations, and method unwinding occur each time. You will not want to use them in any hot paths. Today, In today's post, I will help you decide when exceptions are appropriate and when a simple alternative type might be better.

How .NET handles exceptions internally (and why they're expensive)

What is an Exception?

An exception is an error condition or unexpected behaviour during the execution of a program. Exceptions can occur at runtime for various reasons, such as accessing a null object, dividing by zero, or requesting a file that is not found. A C# exception contains several properties, including a Message describing the cause of the exception. StackTrace contains the sequence of method calls that led to the exception in reverse call order to trace the exception source.

How does an exception work?

The try block encloses the code prone to exceptions. try/catch protects the application from blowing up. Use the throw keyword to signal the error and throw an Exception object containing detailed information, such as a message and a stack trace. The caught exception allows the program to continue gracefully and notify the user where and what error occurred. When an error occurs, the CLR searches for a compatible catch block in the current method. If not found, it moves up the call stack to the calling method, and so on. Once a matching catch is found based on the exception type, control jumps to that block. In an unhandled exception situation where no compatible catch block is found, the application can terminate. Exception handling uses a heap to store the message. To look for a catch body, the CLR unwinds the stack by removing intermediate stack frames. The JIT must generate EH tables and add hidden control-flow metadata.

What is OneOf<T> in .NET?

OneOf<T> or OneOf<T1, T2, T...> represents a discriminated union containing all possible returns of an operation or a method. It contains an array of types, allowing a method to return one of several defined possibilities. The OneOf pattern provides you with fine-grained control and type safety.

Examine Exceptions with the benchmark.

To truly understand it, let's create an application. I will use a console application.

Step 1: Create the project

dotnet new console -n ExceptionBenchmark
cd ExceptionBenchmark

Step 2: Add necessary packages

I am adding the Benchmark library along with OneOf, which is used for the OneOf return type.

dotnet add package BenchmarkDotNet
dotnet add package OneOf

Step 3: Set up the program.cs

All the code is in the Program.cs

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using OneOf;

BenchmarkRunner.Run<ExceptionBenchmarks>();

[MemoryDiagnoser] 
public class ExceptionBenchmarks
{
    private const int Iterations = 100_000;
    private const int FailureEvery = 10;

    [Benchmark]
    public int NoException()
    {
        int failures = 0;

        for (int i = 1; i <= Iterations; i++)
        {
            if (!DoWork_NoException(i))
                failures++;
        }

        return failures;
    }

    private bool DoWork_NoException(int i)
    {
        return i % FailureEvery != 0;
    }

    [Benchmark]
    public int WithException()
    {
        int failures = 0;

        for (int i = 1; i <= Iterations; i++)
        {
            try
            {
                DoWork_WithException(i);
            }
            catch
            {
                failures++;
            }
        }

        return failures;
    }

    private void DoWork_WithException(int i)
    {
        if (i % FailureEvery == 0)
            throw new InvalidOperationException();
    }

    [Benchmark]
    public int WithOneOf()
    {
        int failures = 0;

        for (int i = 1; i <= Iterations; i++)
        {
            var result = DoWork_WithOneOf(i);

            if (result.IsT1)
                failures++;
        }

        return failures;
    }

    private OneOf<Success, Error> DoWork_WithOneOf(int i)
    {
        if (i % FailureEvery == 0)
            return new Error("Error");

        return new Success("Passed");
    }

    private readonly struct Success
    {
        public string Message { get; }

        public Success(string message)
        {
            Message = message;
        }
    }

    private readonly struct Error
    {
        public string Message { get; }

        public Error(string message)
        {
            Message = message;
        }
    }
}

The first method is simple with no exception. Then it throws an exception, and in subsequent methods, it finally returns an error object from OneOf. To make it realistic, each method will observe with 10% error and 90% success rate, as FailureEvery is set to 10. Success and Error are value types to avoid allocations, since they only return the value from the method.

Step 4: Run and test

dotnet run -c Release
Benchmark results

The best performer is the NoException. But that is not practical, you have to identify unexpected behaviour and report it in the code flow. Firstly, a naive approach is to use an exception. Using it adds a time cost and increases the Garbage collector's Gen 0 pressure. So, our alternative to exception is OneOf, which significantly saved time and memory. We can further add Objects to the OneOf, considering the possible return values of the method.

In exceptions, Stack tracing is very expensive, as it propagates stacks, captures method names, stores IL offsets, and inspects frames. Also, the JIT inlining is limited during exceptions. With OneOfI used a struct value type, so Gen 0 utilization is minimized. Neither does it fall for stack trace nor unwind it. Hence, the execution remains linear.

When can I use an exception alternative?

In the following cases, exceptions can be replaced with Result or OneOf in normal application flows.

  • Business rule rejection, such as the customers cannot order out-of-stock items. You can return an error in response.
  • API validation, where you can simply return 400 with a custom message after figuring out all possible error cases.
  • High-throughput paths where you cannot afford an exception mechanism.
  • Validation failure, such as invalid email or mobile number input.
  • Data not found scenarios where you know either the request data will be available or will not be found. Simply, you can deal with both cases.

When is an exception the optimal choice?

You don't remove fire alarms from a building because they're loud. You just don't pull them every time someone burns toast. We have some situations where exceptions stand out even if they are expensive.

  • Exceptions occur when the program falls into an impossible state, such as when a null database connection is used. You cannot proceed anywhere because the connection is not even initialized for some reason.
  • For environmental failures, you will opt for exceptions such as timeout failures, disk I/O failures, or database connection losses.
  • Programming bugs where your code falls into a dead end, and it cannot handle further. Conditions where your input case exhaust, such as you have order statuses of pending, cancelled, and confirmed, are enumerated with 1,2 and 3, respectively. There is no case apart from that, so you can simply throw an ArgumentOutOfRangeException or a custom exception in the default case.
  • If developing a library, use an exception to signal to the user what went wrong and halt normal execution. Here, you cannot force consumers to handle result types.

Conclusion

In high-performance systems, every allocation matters. Exceptions aim to provide a safeguard against anomalous conditions, but they can sometimes be a burden on memory and CPU. I put light on the exception of how much resource they can use for simple operations compared to their counterparts. We explored where it is suitable and where it can be replaced. In short, use exceptions for Unexpected, impossible, and environmental failures. While you can simply use Result/OneOf As an alternative, when conditions are expected, it is useful for business validation, user-driven errors, and high-frequency failures.

Code: https://github.com/elmahio-blog/ExceptionBenchmarks.git



Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

GitHub Copilot Agent vs. GitHub Copilot Code Review

1 Share
Agents are all the rage today, but what happens when we start using multiple agents together? Can we improve the overall accuracy of our results? Can we reduce the total time to develop a feature? Let's dig in and find out!
Read the whole story
alvinashcraft
20 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

VS Code Memory Tool: Local Memory meets GitHub Copilot Memory

1 Share

A few weeks ago I wrote about Copilot Memory in VS Code - the GitHub-hosted system that lets Copilot learn repository-specific insights across agents. Since then, VS Code has shipped a second, complementary memory system: the Memory tool. These two systems solve related but distinct problems, and understanding both helps you get the most out of Copilot in your daily workflow.

What is the Memory Tool?

The Memory tool is a built-in agent capability that stores notes locally on your machine. Unlike Copilot Memory, which lives on GitHub's servers and requires a GitHub repo to function, the Memory tool writes plain files to your local filesystem and reads them back at the start of each session.

You enable or disable it with the github.copilot.chat.tools.memory.enabled setting. It's on by default.


Three memory scopes

VSCode organizes memories into three scopes:

Scope Persists across sessions Persists across workspaces Good for
User Personal preferences, habits
Repository Project conventions, architecture
Session In-progress task context

User memory is the most broadly applicable. The first 200 lines load automatically into the agent's context at the start of every session, across every workspace. Ask the agent something like:

Remember that I like to use XUnit as my preferred testing framework.

...and it will apply that preference every time, regardless of which project you open.

Repository memory is scoped to the current workspace. This is the right place to capture things like "this project uses the repository pattern for data access" or "all API endpoints require authentication." That context persists across sessions in that workspace but doesn't bleed into other projects.

Session memory clears when the conversation ends. The planning agent uses this to store its plan.md file — useful for multi-step tasks within a single session, but intentionally ephemeral.

Using the memory tool in practice

Storing a memory is just natural language:

Remember that our teams use XUnit as our preferred testing framework.

Retrieving it in a new session is equally straightforward:

What testing framework is used?

The agent checks the memory files and surfaces the relevant information. References to memory files in chat responses are clickable, so you can inspect the raw content directly.

Two commands help you manage what's stored:

  • Chat: Show Memory Files — opens a list of all memory files across scopes
  • Chat: Clear All Memory Files — wipes everything

 


Memory Tool vs. Copilot Memory: side by side

This is the comparison that matters if you've already been using Copilot Memory (also see the documentation):

  Memory Tool Copilot Memory
Storage Local (your machine) GitHub-hosted
Scopes User, repository, session Repository only
Shared across Copilot surfaces No (VS Code only) Yes (coding agent, code review, CLI)
Who creates memories You or the agent during chat Copilot agents automatically
Enabled by default Yes No (opt-in)
Expiration Manual Automatic (28 days)
Requires GitHub repo No Yes

The practical split: use the Memory tool for personal preferences and anything workspace- or session-specific that you control. Use Copilot Memory for repository knowledge that should propagate across GitHub's Copilot surfaces — coding agent, code review, CLI.

Also worth remembering from my previous post: Copilot Memory currently only works when your repository is hosted on GitHub. If you're on Azure DevOps or a local-only repo, the Memory tool is your only option.

Where does this leave us?

When I wrote about Copilot Memory back in February, one of the things I hoped for was better support for non-GitHub sources. The Memory tool partially addresses that gap — it's source-agnostic and works entirely offline. But it's also more manual; you drive what gets remembered, rather than agents picking it up automatically.

The two systems aren't competing — they're designed to be complementary. What's still missing is a unified view across both, and better tooling for organizing and reviewing what's been stored over time. That's probably where the feature evolves next.

More information

Memory in VS Code agents

Enabling and curating Copilot Memory

Copilot Memory in VS Code: Your AI assistant just got smarter

Read the whole story
alvinashcraft
35 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Aspire 13.2.1

1 Share

.NET Aspire 13.2.1 Release Notes

Bug Fixes

  • Fix cross-compiled CLI bundles missing DCP — win-arm64, linux-arm64, and linux-musl-x64 CLI bundles now correctly include DCP instead of silently
    producing broken bundles (#15529)
  • Fix aspire new in VS Code — OpenEditor is now called after the agent init prompt completes, preventing the workspace switch from severing the CLI
    terminal mid-interaction (#15553)
  • Fix dashboard describe command URLs — Strip /login?t=... from dashboard base URL so resource links no longer produce broken URLs (#15495)
  • Fix guest AppHost launch profile env propagation — Environment variables from launch profiles are now correctly forwarded to guest AppHosts (#15637)
  • Fix .aspire/settings.json → aspire.config.json migration — The migration was silently skipped when the AppHost was resolved from legacy settings
    (#15526)
  • Fix TypeScript AppHost restore config resolution (#15625)
  • Fix installing playwright-cli via aspire agent init on Windows (#15559)
  • Pin Kusto emulator image and fix Cosmos DB emulator stability (#15504)

Improvements

  • Brownfield TypeScript aspire init — Running aspire init in existing JS/TS projects now intelligently merges package.json (scripts, dependencies,
    engines) with semver-aware conflict handling (#15123)
  • Endpoint filtering from default reference set — New ExcludeReferenceEndpoint property allows filtering specific endpoints from WithReference (#15586)
  • Export more ATS hosting APIs for polyglot (TypeScript/Go) AppHost authoring (#15557)
  • Dashboard: support short trace IDs in TelemetryApiService.GetTrace (#15613)
  • Deprecate Aspire.Hosting.NodeJs in CLI integration listings — replacement is Aspire.Hosting.JavaScript (#15686)
Read the whole story
alvinashcraft
46 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Microsoft says Copilot ad in GitHub pull request was a bug, not an advertisement

1 Share

Developers have alleged that Copilot ads are showing up in GitHub pull requests. However, Microsoft has denied the reports and told Windows Latest that it does not plan to show ads on GitHub. As for alleged Copilot-generated ads on GitHub, the company says it was a bug, and not an intentional move.

Thousands of pull requests on GitHub appear to include a Copilot-generated product tip, which looks more like an advertisement than anything else. This came to our attention when Zach Manson, a software developer based in Melbourne, spotted a Copilot-generated ad or suggestion in his project’s pull request on March 30.

This Copilot ad showed up after a team member in Zach’s project requested Copilot in GitHub to correct an error in a pull request. And that’s not an expected behaviour.

Copilot ad on Github
Copilot-generated product tip or “ad” on GitHub PR

GitHub’s Copilot integration is actually quite helpful, and developers use it frequently, especially if they want to clean up their pull requests. But to Zach’s surprise, GitHub inserted a small ad for Copilot’s agentic features and Raycast, which is a popular third-party search tool on macOS and Windows.

“This is horrific. I knew this kind of bull**** would happen eventually, but I didn’t expect it so soon,” Manson wrote in a post.

Since the post went viral on Hacker News, Raycast developers have denied entering into an ad arrangement with Microsoft.

Microsoft says GitHub won’t include ads, and Copilot’s ad-like pull request note was just a bug

In a statement, Microsoft told Windows Latest that GitHub was not testing ads in pull requests.

Instead, the company said a bug caused existing Copilot product tips, including one referencing Raycast, to appear in the wrong place.

According to Microsoft, these tips were originally meant only for pull requests created by Copilot, but the bug made them show up in some human-created pull requests as well when Copilot was invoked to edit code.

“GitHub does not and does not plan to include advertisements in GitHub,” says Martin Woodward, VP of Developer Relations at GitHub, in a statement to Windows Latest. “We identified a programming logic issue with a GitHub Copilot coding agent tip that surfaced in the wrong context within a pull request comment. We have removed agent tips from pull request comments moving forward.”

The original GitHub announcement from last week appears to support Microsoft’s explanation, at least in part.

GitHub feature that allows you to add Copilot to PR
GitHub feature that allows you to add Copilot to PR

GitHub had already said Copilot could be mentioned directly in pull requests to make changes, and also noted that Copilot previously opened its own pull requests on top of existing ones.

“Copilot would open a new pull request on top of your existing pull request, using the existing pull request’s branch as its base branch,” GitHub noted in a release notes update on March 24, 2026.

Microsoft sources also confirmed to Windows Latest that it was an unintentional behavior of GitHub’s new Copilot feature, which allows you to invite AI to make changes to pull requests.

When the company rolled out the feature, a product tip from the GitHub Copilot coding agent that included a third-party suggestion was inadvertently displayed in the main pull request comment when Copilot was called by a developer.

Microsoft identified this behavior as a programming logic issue and removed the agent tips feature from pull request comments.

The post Microsoft says Copilot ad in GitHub pull request was a bug, not an advertisement appeared first on Windows Latest

Read the whole story
alvinashcraft
3 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET Aspire 13.2 Adds AI-Agent CLI, TypeScript AppHost Support

1 Share
Microsoft has released .NET Aspire 13.2 with a new AI-focused CLI for coding agents, preview TypeScript AppHost support, dashboard updates, and revised integrations.
Read the whole story
alvinashcraft
3 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories