January 23rd, 2026
intriguinglikeheart6 reactions

C++ has scope_exit for running code at scope exit. C# says “We have scope_exit at home.”

The Windows Implementation Library (commonly known as wil) provides a helper called scope_exit which creates and returns an RAII object whose destructor runs the lambda you specify. This helper provides roughly equivalent functionality in C++ to what is called tryfinally in other languages such as C#.

// If there is an active primary Gadget, then do a bunch of stuff,
// but no matter what, make sure it is no longer active when finished.
var gadget = widget.GetActiveGadget(Connection.Primary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
    } finally {
        widget.SetActiveGadget(Connection.Primary, null);
    }
}

One thing that is cumbersome about this pattern is that the cleanup code is far away from the place where the cleanup obligation was created, making it harder to confirm during code review that the proper cleanup did occur. Furthermore, you could imagine that somebody makes a change to the GetActiveGadget() call that requires a matching change to the SetActiveGadget(), but since the SetActiveGadget() is so far away, you may not realize that you need to make a matching change 200 lines later.

var gadget = widget.GetActiveGadget(Connection.Secondary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
    } finally {
        widget.SetActiveGadget(Connection.Secondary, null);
    }
}

Another thing that is cumbersome about this pattern is that you may create multiple obligations at different points in the code execution, resulting in deep nesting.

var gadget = widget.GetActiveGadget(Connection.Secondary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
        if (gadget.IsEnabled()) {
            try {
                ⟦ lots more code ⟧
            } finally {
                gadget.Disable();
            }
        }
    } finally {
        widget.SetActiveGadget(Connection.Secondary, null);
    }
}

Can we get scope_exit ergonomics in C#?

You can do it with the using statement introduced in .NET 8 and a custom class that we may as well call Scope­Exit.

public ref struct ScopeExit
{
    public ScopeExit(Action action)
    {
        this.action = action;
    }

    public void Dispose()
    {
        action.Invoke();
    }

    Action action;
}

Now you can write

var gadget = widget.GetActiveGadget();
if (gadget != null) {
    using var clearActiveGadget = new ScopeExit(() => widget.SetActiveGadget(null));
    ⟦ lots of code ⟧
    if (gadget.IsEnabled()) {
        using var disableGadget = new ScopeExit(() => gadget.Disable());
        ⟦ lots more code ⟧
    }
}

, Although many objects implement IDisposable so that you can clean them up with a using statement, it’s not practical to have a separate method for every possible ad-hoc cleanup that could be needed, and the ScopeExit lets us create bespoke cleanup on demand.¹

I’m adding this to my list of “Crazy C# ideas which never go anywhere,” joining CastTo<T> and As<T> and ThreadSwitcher.

¹ There might be common patterns like “If you Open(), then you probably want to Close()” which could benefit from a disposable-returning method, but you still have to wrap the result.

public ref struct OpenResult
{
    public bool Success { get; init; }

    public OpenResult(Widget widget)
    {
        this.widget = widget;
        Success = widget.Open();
    }

    public void Dispose()
    {
        if (Success) {
            widget.Close();
        }
    }

    private Widget widget;
}
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

15 comments

Discussion is closed. Login to edit/delete existing comments.

Sort by :
  • Maik Schott

    Instead of just

    action.Invoke()

    I would call

    Interlocked.Exchange(ref action, null)?.Invoke();

    . This follows the contract of IDisposable.Dispose which states that multiple calls to Dispose should only dispose once.

    Also since C# 13 ref structs allow interfaces. Therefore we can let it explicitly implement IDisposable instead of just duck-typing it.

    • GL

      There's no point in doing interlocked operations on fields of ref structs, because (1) they only live as a thread's local variables, (2) it's undefined behavior to access another thread's local variables (see .NET / unsafe code best practices / 8. cross-thread access to local variables). Consequently, a ref struct can only be operated by one thread, so the interlocked exchange can be optimized to read-and-reset.

      On the interface, did you mean "we can let it (implicitly) implement `IDisposable`"? It's a bad and unnecessary idea to explicitly implement `IDisposable` on ref structs, because (1) it'll be more difficult to call `IDisposable.Dispose`...

      Read more
  • Otul Osan · Edited

    On Windows 9x, what was the use cases for C:\WINDOWS\WINSTART.BAT compared to C:\AUTOEXEC.BAT, and at which phase of the startup process it's launched?

    Could we see an explanation of the whole Windows 9x startup and shutdown process one day? On Wikipedia, The 16-bit modules are loaded doesn't tell what precisely technically happens after WIN.COM is launched, besides loading modules.
    For example I don't know what module switches to VGA mode in the boot process, draws the busy cursor on black background for 2 seconds and then paints the wallpaper.

    Read more
    • Neil Rashbrook

      I see even JavaScript is getting this now, I believe it looks something like this:

      function ScopeExit(callback) {
        return { [Symbol.dispose]: callback };
      }

      Then you can use

      using clearActiveGadget = ScopeExit(() => widget.SetActiveGadget(null));

      (Yes you can do this with classes if you like.)

    • Neil Rashbrook

      I think it was used for TSRs that were only needed in protected mode? The only one I know of was Novell’s BREQUEST for BTRIEVE. Windows BTRIEVE applications want to make calls to BREQUEST. MS-DOS BTRIEVE applications tended to have it statically linked. This is all a very hazy memory but it’s the best I can do, sorry.

    • Dan Bugglin

      AUTOEXEC.BAT was executed by MS-DOS as part of the boot process (the second to last step before launching COMMAND.COM I think). Win 9x shipped on top of MS-DOS 7.x so that meant you got AUTOEXEC.BAT and the full MS-DOS trimmings. Windows NT was built from the ground up to not need MS-DOS so no MS-DOS stuff.

      I checked Windows 98 and Windows 3.1 VMs I have, neither have a WINSTART.BAT, and I have not heard of it before despite using Windows since 3.1. Back in those days you just ran WIN.COM to launch Windows, though sometimes you wanted to switch out...

      Read more
      • Otul Osan · Edited

        If you want to try (Windows 95), WINSTART.BAT is executed at a later stage in the boot process in protected mode, create a file in C:\WINDOWS\WINSTART.BAT and put COMMAND.COM as the content.

  • George Styles

    Ive used this type of approach before when writing my own profiler. Start the method with

    using (new MyProfiler("LoadData")) 

    records the tick time in the constructor, and it automatically calls the dispose method when scope is exitted, at which point we calculate how many ticks and record.

  • GL · Edited

    It rightly is a crazy idea that should never go anywhere, because that type doesn't give the similar level of guarantee as WIL scope_exit.

    Allocation of the delegate could fail at the lambda expression. This creates a window where exception can be thrown (I'm ignoring asynchronous thread aborts, which is either impossible in new .NET or can be guarded by CER in .NET Framework) but the scope-exit action is not set up. For non-capturing lambdas, under current optimization, the failure could happen the first time that line is executed. For capturing lambdas, it introduces an allocation each time that line is...

    Read more
    • Jacob Manaker

      Like many exception-safety problems, that’s fixable by re-ordering code. Just put the ScopeExit creation prior to use:

      Gadget gadget = null;
      using var clearActiveGadget = new ScopeExit(() => widget?.SetActiveGadget(null));
      gadget = widget.GetActiveGadget();
      if (gadget != null) {
          ⟦ lots of code ⟧
      }

      The gadget-enablement code doesn’t need a rearrangement, AFAICT.

      • GL · Edited

        Re: Jacob Manaker.

        The scope-exit objects / delegate need to be created and not `using`ed until a condition holds (e.g., the `Disable` call in the blog article), or, if all scope-exit objects are `using`ed as soon as created, an explicit Boolean variable is needed to indicate whether clean-up becomes needed. However, in case of using-loop-using nesting, it's possible that the inner `using` delegates might only be creatable inside the loop (say, because it captures a variable inside the loop), which unnecessarily creates new failure points inside the loop.

        Re: Kristof Roomp.

        Recoverability from exceptional state and real-time-ness are two orthogonal things. On "shared...

        Read more
    • Matthew Smit

      To be honest, if it fails because of an allocation, then you have bigger problems. Your application will crash pretty soon after anyway, since it’s likely that the next exception thrown would cause an OOM anyway.

      Generally speaking, if you need functionality to do something like this, have a class that has the logic in IDisposable.

      • Kristof RoompMicrosoft employee

        Kind of like worrying about whether you watered your plants while the house is burning down... just abort and get out of there. Maybe not if you are doing a real time control system, but C# is probably not a suitable language for that in any case.

        Protecting a moderately complicated library to maintain 100% consistency even with OOM exceptions is really hard, eg you have shared state protected by locks, you blow up half-way through an update and what do you do? some kind of fancy tainting scheme? then whoever is calling you has to deal with the fact that...

        Read more
      • GL

        It’s unclear what “next exception” is, but when allocation fails, an OutOfMemoryException is always available (the CLR pre-allocates one). I agree that failing to allocate a delegate is almost unrecoverable, but such knowledge is only definite for code with full knowledge of the application — if the code is inside a component or library, the author has no business in setting the OOM exception recovery policies. For example, the back-out path could stop referring to a large chunk of managed memory, thus releasing the memory pressure and allowing the application to gracefully continue.