2
votes

Update 1: Originally, I posted this with the title: "Scripts ignoring error handling in PowerShell module" as that is the current issue, however it seems more of a module issue, so I have renamed the title.

Update 2: After a comment that made me question Azure cmdlets, I've tested with the most basic of scripts (added to the module) and the findings are the same, in that the error is not passed to the calling script, however, adding -errorVariable to Get-Service does return something (other than WriteErrorException) that I could probably harness in the handling of the error:

function Test-MyError($Variable)
{
    Try
    {
        Get-Service -Name $variable -ErrorAction Stop -ErrorVariable bar
        #Get-AzureRmSubscription -SubscriptionName $variable -ErrorAction Stop
    }
    Catch
    {
        Write-Error $error[0]
        $bar
    }
}

returns:

Test-MyError "Foo"

Test-MyError : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError

The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'foo'.

However, if I run "Test-MyError" in ISE, then call the function, I get:

Test-MyError "Foo"

Test-MyError : Cannot find any service with service name 'Foo'.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError

The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'Foo'.

So I am not sure what is happening when running "Test-MyError" in ISE and calling it, against it being dot-sourced in the PSM1 file and then calling it?

Do I now have to use -ErrorVariable and handle on that?

Original Question: I have two functions in a module: Get-Subscription and Get-AllSubscriptions. Each function sits in its own PS1 file and the PSM1 file dot-sources them. The module seems fine as the module scripts are accessible using intelisense and the module loads without issue. I've used this structure in many modules and I haven't come across this problem before. (Although I wonder if MS have changed the way modules work in PS 5.1 as I have noticed using FunctionsToExport='x','y','Z' and Export-ModuleMember don't seem to behave the same way as they used to.)

Get-AllSubscriptions calls Get-Subscription.

If I am not logged into Azure, Get-Subscription should throw an error which is handled, prompting me to log in. This works as expected, if I call Get-Subscription from the Get-Subscription.ps1.

However, when I call Get-Subscription from the a new PS1 file, Get-AllSubscriptions or from the powershell console, it doesn't work. It iterates all the way through the do..until loop, without "handling" the errors as I would expect. On each iteration, it seems to throw a generic error:

Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.

However, I do see the last error, Get-Subscription : Unable to find requested subscription after 3 login attempts.

If I execute Get-Subscription in ISE, then call Get-Subscription in a new PS1 file or from Get-AllSubscriptions, it works as expected, however, once I re-import the module (Import-Module AzureVnetTools -Force -Verbose), it goes back to the incorrect behaviour.

If I dot-source Get-Subscription, inside the caller script, it works, but why? This is what should happen with the module's PSM1.

Can anyone help me work out what I am doing wrong here?

(PS 5.1, Windows 7)

Get-Subscription:

function Get-Subscription
{
    [cmdletbinding()]
    Param
    (
        [string]$SubscriptionName,
        [string]$UserName,
        [string]$code
    )

    $c=1
    Write-Verbose "Checking access to '$SubscriptionName' with user '$UserName'..."
    Do
    {
        Write-Verbose "Attempt $c"
        Try
        {
            $oSubscription = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -ErrorAction Stop -WarningAction SilentlyContinue

            Write-Verbose "Subscription found: $($oSubscription.SubscriptionName)."
        }
        Catch
        {
            if($error[0].Exception.Message -like "*Please verify that the subscription exists in this tenant*")
            {
                Write-Verbose "Cannot find subscription '$SubscriptionName' with provided credentials."
                $account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
            }
            elseif($error[0].Exception.Message -like "*Run Login-AzureRmAccount to login*")
            {
                Write-Verbose "No logged in session found. Please log in."
                $account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
            }
            else
            {
                Write-Error $error[0]
            }
        }
        $c++
    }
    until(($oSubscription) -or ($c -eq 4))

    if($c -eq 4)
    {
        Write-Error "Unable to find requested subscription after $($c-1) login attempts."
        break
    }
    $oSubscription | Add-Member -MemberType NoteProperty -Name Code -Value $code
    $oSubscription
}

Get-AllSubscriptions:

function Get-AllSubscriptions
{
    [cmdletbinding()]
    param
    (
        [string]$MasterSubscription,
        [string]$MasterSubscriptionCode,
        [string]$MasterSubscriptionUsername,
        [string]$ChildSubscription,
        [string]$ChildSubscriptionCode,
        [string]$ChildSubscriptionUsername
    )

    Write-Verbose "Getting all subscriptions..."

    $oAllSubscriptions = @()

    $oMasterSubscription = Get-Subscription -SubscriptionName $MasterSubscription -UserName $MasterSubscriptionUsername -code $MasterSubscriptionCode -Verbose

    $oChildSubscription = Get-Subscription -SubscriptionName $ChildSubscription -UserName $ChildSubscriptionUsername -code $ChildSubscriptionCode -Verbose

    $oAllSubscriptions = ($oMasterSubscription,$oChildSubscription)
    $oAllSubscriptions

}

Test:

$splat2 = @{
    SubscriptionName = "SomeSubscription"
    Code = "S02"
    Username = "[email protected]"
}

#Write-Output "Dot-source:"
#. "D:\Temp\PS.Modules\AzureVnetTools\functions\public\Get-Subscription.ps1"

Get-Subscription @splat2 -verbose

Output:

Get-Subscription @splat2 -verbose
VERBOSE: Checking access to 'SomeSubscription' with user '[email protected]'...
VERBOSE: Attempt 1
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription @splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription

VERBOSE: Attempt 2
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription @splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription

VERBOSE: Attempt 3
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription @splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription

Get-Subscription : Unable to find requested subscription after 3 login attempts.
At line:7 char:1
+ Get-Subscription @splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription

AzureVnetTools.psm1

#Get public and private function definition files.
$Public  = @( Get-ChildItem -Path $PSScriptRoot\Functions\Public\*.ps1 -ErrorAction SilentlyContinue )
$Private = @( Get-ChildItem -Path $PSScriptRoot\Functions\Private\*.ps1 -ErrorAction SilentlyContinue )

#Dot source the files
Foreach($import in @($Public + $Private))
{
    #write-error $import.fullname
    Try
    {
        #Write-Host "Dot-sourcing file: $($import.fullname)." 
        . $import.fullname
    }
    Catch
    {
        Write-Error -Message "Failed to import function $($import.fullname): $_"
    }
}

Export-ModuleMember -Function $Public.Basename

AzureVnetTools.psd1 (Relevant section):

FunctionsToExport = '*'
2
Sounds like Write-Error $Error[0] is being evaluated due to an unhandled exception. Change to Write-Host $Error[0] to see if will print something useful.G42
@woter324 What type is your account? Microsoft account or Azure AD account? Do you check your subscription name right?Shui shengbao
@gms0ulman Thanks, tried your suggestion, but no difference.woter324
@walter - MSFT It's a AAD account, but I don't think that is the problem. Subscription is correct.woter324
@woter324 What is your Azure PowerShell version? I test your script in my lab, I don't meet your error. You could use Get-Module -ListAvailable -Name Azure -Refresh to check your Azure PowerShell version. The latest version is 4.10.Shui shengbao

2 Answers

0
votes
-ErrorAction Stop -WarningAction SilentlyContinue

Is it a warning that's being thrown instead of an error?

0
votes

So my specific problem was that I was relying on handling the error and doing something based on that. The problem was caused by the way PowerShell's Write-Error works (or not) as I learned from the reply here, given by @Alek.

It simply wasn't passing the actual error back to the calling script. As @Alex suggested, I replaced Write-Error with $PSCmdlet.WriteError(). Although this didn't totally work.

In the Catch{} block, I then changed $error[0] to $_ and the full error was returned to the calling script / function.

I went one further and wrote a reusable function, added to my module:

function Write-PsError
{
    [cmdletbinding()]
    Param
    (
        [Exception]$Message,
        [Management.Automation.ErrorCategory]$ErrorCategory = "NotSpecified"
    )

    $arguments = @(
            $Message
            $null #errorid
            [Management.Automation.ErrorCategory]::$ErrorCategory
            $null

            )

    $ErrorRecord = New-Object -TypeName "Management.Automation.ErrorRecord" -ArgumentList $arguments
    $PSCmdlet.WriteError($ErrorRecord)

}

Which seems to be working well at the moment. I especially like the way intellisense picks up all the ErrorCategories. Not sure what or how ISE (PS 5.1 / Win 7) does that. I thought I was going to have to add my own dynamic parameter.

HTH.