Skip to content

Allow user code to stop a pipeline on demand / to terminate upstream cmdlets. #3821

Open

Description

The ability to stop a pipeline on demand is currently only available internally, as used by Select-Object -First <n>, for instance.

To quote from this uservoice.com issue:

It is of great value to be able to stop the pipeline if your mission is completed before the emitting cmdlet has provided all results.

In fact, the PS pipeline would gain a completely new "feature". Currently, it is a memory throttle and provides realtime feedback at the cost of performance. If stopping the pipeline was something available to users, it could also be a generic means of running cmdlets only partially.

Select-Object is a great start but you do not always know beforehand how many elements you need. So it would be beneficial to have a Stop-Pipeline cmdlet or at least a public StopUpstreamCommandsException that a script programme can throw.

A real-world example.

Also note that there are two distinct ways to "stop" a pipeline:

  • Exiting the entire pipeline instantly (effectively aborting it) - with downstream cmdlets not getting a chance to run their End blocks, as simulated by this command using break with a dummy loop to break out of:
# Produces NO output, because Sort-Object's End block never runs.
do { 
  1..10 | % { if ($_ -gt 2) { break }; $_ } | Sort-Object -Descending
 } while ($False)

Note that without the final Sort-Object -Descending pipeline stage, you would see output, because the objects are being output in the % script block as they're being received.

This quiet termination of the entire pipeline is similar to a statement-terminating error that is silenced.


  • Stopping only the upstream cmdlets (the cmdlet calls in earlier pipeline stages), as Select-Object -First <n> does, for instance, giving downstream cmdlets a chance to run their End blocks.
# Produces (2, 1), because Sort-Object's End block IS run.
1..10 | Select-Object -First 2 | Sort-Object -Descending

This scenario is about stopping further input, while still allowing remaining pipeline stages to finish their processing.

However, the fact that the upstream cmdlets do not also get to run their End blocks can be problematic, as that may be required for cleanup tasks: see #7930; PR #9900 would instead introduce a cleanup block to ensure cleanup even when stopped via StopUpstreamCommandsException.


Originally reported for:

PowerShell Core v6.0.0-beta (v6.0.0-beta.1)

Activity

  1. added
    WG-Enginecore PowerShell engine, interpreter, and runtime
    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassif
    on May 19, 2017
  2. added this to the milestone on May 19, 2017
  3. dragonwolf83 commented on May 20, 2017

    @dragonwolf83

    There is a good detailed blog post on how to implement this in PowerShell today: Cancelling a Pipeline. I think it would be useful to implement something like as a built-in cmdlet.

  4. mklement0 commented on May 20, 2017

    @mklement0
    ContributorAuthor

    Thanks, Troy Campbell (@dragonwolf83); the blog post's author, Tobias Weltner, is the same person who created the linked uservoice.com issue; the blog post elicited the following Jeffrey Snover (@jpsnover) response (dated "over 7 years ago"):

    We have wanted to add pipeline stopping for quite a while and it never got raised above the bar.

    That's one lofty bar! :)

  5. SteveL-MSFT commented on May 20, 2017

    @SteveL-MSFT
    Member

    Things that don't necessarily align with the PowerShell Team's priorities (which is not static) can still get done if contributed by the community :)

  6. iSazonov commented on May 20, 2017

    @iSazonov
    Collaborator

    We need RFC for new cmdlet and examples of how to use the cmdlet.

  7. changed the title [-]Allow user code to stop a pipeline on demand[/-] [+]Allow user code to stop a pipeline on demand / to terminate upstream cmdlets.[/+] on Aug 13, 2017
  8. alx9r commented on Jan 1, 2018

    @alx9r

    Does anyone have a working link to Tobias' blog post mentioned above? That link seems to be dead.

  9. TobiasPSP commented on Jan 2, 2018

    @TobiasPSP
    Collaborator
  10. SeeminglyScience commented on Jun 20, 2022

    @SeeminglyScience
    Contributor

    Adding Needs-Triage so WG-Engine can discuss it. This comes up surprisingly often. Typically for profile functions, but still. The lengths one has to go through to get the same behavior is pretty wild (and unsupported).

    My proposal:

      namespace System.Management.Automation
      {
          public abstract class PSCmdlet
          {
    +         [DoesNotReturn]
    +         public void StopUpstreamCommands();
          }
      }

    If added this should be tested both for binary cmdlets (this.StopUpstreamCommands()) and script cmdlets ($PSCmdlet.StopUpstreamCommands())

  11. 2 remaining items

  12. added
    WG-ReviewedA Working Group has reviewed this and made a recommendation
    and removed
    WG-NeedsReviewNeeds a review by the labeled Working Group
    on Apr 21, 2025
  13. SeeminglyScience commented on Apr 21, 2025

    @SeeminglyScience
    Contributor

    The Engine WG discussed this today, we agree this would be a useful feature. We should also ensure that the exception thrown by the method is documented so folks know to rethrow it when surrounding with a try. Also we should ensure that it works properly when thrown from the first pipeline position.

  14. added
    Up-for-GrabsUp-for-grabs issues are not high priorities, and may be opportunities for external contributors
    on May 20, 2026
  15. TobiasPSP commented on May 20, 2026

    @TobiasPSP
    Collaborator

    Here is a prototype taking advantage of the existing behavior in Select-Object:

    function Stop-Pipeline
    {
      $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Select-Object', [System.Management.Automation.CommandTypes]::Cmdlet)
      $scriptCmd = {& $wrappedCmd -First 1 }
      $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
      $steppablePipeline.Begin($true)
      $steppablePipeline.Process(1)
      $steppablePipeline.End()
      $steppablePipeline.Dispose()
    }
    
    "Start"
    1..10  | ForEach-Object {  if ($_ -eq 5) { Stop-Pipeline }; $_ }
    "End"

    Just make sure Stop-Pipeline cannot be called outside a pipeline, or else you get the same behavior as with break: it stops your entire script. The exception raised internally by Select-Object must be caught inside a pipeline and not be allowed to escape into the script scope.

  16. SeeminglyScience commented on May 20, 2026

    @SeeminglyScience
    Contributor

    There's a few caveats to that though. Unfortunately I don't remember most of them, but an example is if you switch from ForEach-Object to & { process { it starts acting more like break. If you add CmdletBinding it'll work again.

    There are other scenarios where that occurs but I can never remember them, it's come up before as a possible solution.

    In order to get it to work in all scenarios, this is what I do in my profile (though in case it needs to be said, this is very outside of support).

  17. TobiasPSP commented on May 21, 2026

    @TobiasPSP
    Collaborator

    Thank you for your comment. Your code is raising the magic exception directly whereas the proposed solution from above indirectly does the same, just by using Select-Object as proxy.

    Both ways work reliably. The reason why Select-Object failed in simple functions and worked in advanced functions is probably due to the way how proxy functions work. Occasionally users repurposed autogenerated sample code which may require $PSCmdlet (NULL in simple functions). The suggested solution above does not have this limitation.

    That said, at this time the exception you raise directly is marked non-public, and although unlikely, it may change at any time without notice. So at this point, strictly speaking, only the Select-Object-approach is safe to use.

    However, this approach is also a bit overkill. I would propose to properly document the exception and make it public, then either use this alone (raising the exception does the trick and is a one-liner), or add a simple wrapper cmdlet or keyword for discoverability.

    While at this, maybe someone can look into the break keyword and (a) fix its runaway tendency when used outside loops that crosses even module boundaries and leads to very hard-to-identify cross-module issues currently, and then (b) add the pipeline break functionality to this existing keyword, based on the exception, again including proper safe-guarding (as this exception will also runaway when issued outside a pipeline).

    Making „break“ also work properly in pipelines seems the most intuitive solution even though potential breaking changes would require scrutiny here.

  18. SeeminglyScience commented on May 21, 2026

    @SeeminglyScience
    Contributor

    Both ways work reliably.

    Yeah so I don't remember the specifics, but the proxy does not always work. It's come up a few times in the discord as a potential solution until folks hit the blocker sometime after. If I can find it I'll post it here, but for whatever reason I always forget the exact issue that crops up.

    That said, at this time the exception you raise directly is marked non-public, and although unlikely, it may change at any time without notice.

    Correct! Though both workarounds arguably rely on undocumented implementation detail. I don't suggest the use of either in production code.

    However, this approach is also a bit overkill. I would propose to properly document the exception and make it public, then either use this alone (raising the exception does the trick and is a one-liner), or add a simple wrapper cmdlet or keyword for discoverability.

    Yeah, there are a few proposals out there that need to be discussed. I'm partial to adding a PSCmdlet.StopUpstreamCommands() method personally.

    While at this, maybe someone can look into the break keyword and (a) fix its runaway tendency when used outside loops that crosses even module boundaries and leads to very hard-to-identify cross-module issues currently, and then (b) add the pipeline break functionality to this existing keyword, based on the exception, again including proper safe-guarding (as this exception will also runaway when issued outside a pipeline).

    That one, unfortunately, is by design behavior that is depended on by folks. For example:

    $b = 0..10
    $stuffToSkip = 0, 2, 4, 6, 8, 10
    foreach ($a in $b) {
        $stuffToSkip | % { if ($a -eq $_) { continue } }
        $a
    }

    I would have made it a parse error personally, but it is what it is.

  19. powercode commented on Oct 3, 2026

    @powercode
    Collaborator

    In addition to PSCmdlet.StopUpstreamCommands(), I think we should add a predicate, IsPowerShellControlFlowException(Exception e).

    The idea is that users can then write the following:

    try
    {
    ... mycode
    }
    catch(Exception e) when (!IsPowerShellControlFlowException(e))
    {
    ... myhandler
    }

    Combine that with a Roslyn analyzer, and we have something that could really help users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassifIssue-Enhancementthe issue is more of a feature request than a bugKeepOpenThe bot will ignore these and not auto-closeUp-for-GrabsUp-for-grabs issues are not high priorities, and may be opportunities for external contributorsWG-Enginecore PowerShell engine, interpreter, and runtimeWG-ReviewedA Working Group has reviewed this and made a recommendation

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions