Troubleshooting

PowerShell try/catch Is Not Catching Your Error

You wrapped something in try/catch, it failed, and the catch block never ran. The error printed in red, the script carried on, and whatever you put in the catch was ignored.

This is not a bug and it is not you. PowerShell has two kinds of error and try/catch only sees one of them.

Everything below was run on PowerShell 7.5.8.

The demonstration

try {
    Get-Item 'C:\does\not\exist\nothing.txt'
    "...execution continued past the failing line"
} catch {
    "CAUGHT: $($_.Exception.Message)"
}
Get-Item:
Line |
   6 |      Get-Item $missing
     |      ~~~~~~~~~~~~~~~~~
     | Cannot find path 'C:\does\not\exist\nothing.txt' because it does not exist.
...execution continued past the failing line

The error was written out. The catch block did not run. The line after it did.

Now the same thing with four extra words:

try {
    Get-Item 'C:\does\not\exist\nothing.txt' -ErrorAction Stop
} catch {
    "CAUGHT: $($_.Exception.Message)"
}
CAUGHT: Cannot find path 'C:\does\not\exist\nothing.txt' because it does not exist.

Why

Most cmdlet errors are non-terminating. The cmdlet reports a problem and PowerShell carries on, because when you pipe a thousand items into something you usually want the other 999 to be processed.

try/catch only reacts to terminating errors.

-ErrorAction Stop promotes a non-terminating error to a terminating one for that call. That is the whole trick, and it is why so much published PowerShell has try/catch blocks in it that do nothing at all.

Write-Error is not throw

Worth knowing if you are writing your own functions:

try { Write-Error 'this is a Write-Error' } catch { "caught" }
try { throw 'this is a throw' }             catch { "caught: $($_.Exception.Message)" }
Write-Error: this is a Write-Error
caught: this is a throw

Write-Error raises a non-terminating error and is not caught. throw is terminating and is.

So if you write a function that reports failure with Write-Error, nobody calling it can catch that failure unless they also pass -ErrorAction Stop. Use throw when you mean stop.

Turning it on for a whole script

Rather than putting -ErrorAction Stop on every line:

$ErrorActionPreference = 'Stop'
CAUGHT without -ErrorAction on the cmdlet

Put that at the top of a script and every cmdlet error becomes terminating. For an admin script that changes things, this is almost always what you want – stopping on the first problem beats carrying on and doing half the job.

The important thing to note is that it is scoped. Set it inside a function and it applies there, not to the caller. That is useful, and it also means setting it in your profile does not protect your scripts.

Catching one kind of error and not others

A bare catch catches everything, including the failures you had not thought about. You can be specific:

try {
    Get-Item $path -ErrorAction Stop
} catch [System.Management.Automation.ItemNotFoundException] {
    "matched the typed catch: file not found"
} catch {
    "fell through to the generic catch"
}
matched the typed catch: file not found

To find the type name for something you are actually hitting, let it fail once and ask:

$Error[0].Exception.GetType().FullName
$Error[0].FullyQualifiedErrorId
$Error[0].CategoryInfo.Category
System.Management.Automation.ItemNotFoundException
PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
ObjectNotFound

FullyQualifiedErrorId is the precise one. It names both the problem and the cmdlet that raised it, which is what you want when two different cmdlets can produce the same exception type.

Collecting errors without stopping

Sometimes you want the loop to finish and a list of what went wrong:

Get-Item $path -ErrorAction SilentlyContinue -ErrorVariable itemErr

$itemErr.Count
$itemErr[0].Exception.Message
1
Cannot find path 'C:\does\not\exist\nothing.txt' because it does not exist.

Use +itemErr with a plus sign to append across a loop rather than overwrite each time.

The one that catches everybody: native commands

try/catch does not work on robocopy, git, msiexec or anything else that is not a cmdlet.

try {
    cmd.exe /c "exit 7"
    "no exception was raised"
} catch {
    "caught"
}
no exception was raised

External programs do not raise PowerShell exceptions. They set an exit code, and you have to check it yourself:

cmd.exe /c "exit 7"
$?              # False
$LASTEXITCODE   # 7

Both measured immediately after the call – and that matters, because $? reflects the last thing that happened. Put a Write-Host between the command and the check and you are reading the result of the Write-Host. I made exactly that mistake while testing this post.

So for anything external:

robocopy $source $dest /E
if ($LASTEXITCODE -ge 8) { throw "robocopy failed with $LASTEXITCODE" }

Note the -ge 8. Robocopy uses 0-7 for various kinds of success, so testing -ne 0 would fail every time it copied something. Check what your tool’s exit codes actually mean rather than assuming zero is the only good one.

PowerShell 7 can do this for you

There is a setting for it, and it is off by default:

$PSNativeCommandUseErrorActionPreference   # False

Turn it on, with $ErrorActionPreference set to Stop:

$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try { cmd.exe /c "exit 7" } catch { "caught = true" }
caught = true

A non-zero exit code now raises a terminating error you can catch like anything else. Worth knowing, and worth being careful with – anything that uses non-zero exit codes for non-failures, robocopy included, will start throwing at you.

finally

try {
    Get-Item $path -ErrorAction Stop
} catch {
    "catch ran"
} finally {
    "finally ran"
}
catch ran
finally ran

Runs whether or not anything failed. Use it to disconnect, release, or clean up temporary files – the things that should happen regardless.

What I actually put in a script

$ErrorActionPreference = 'Stop'

try {
    # everything that changes something
}
catch {
    Write-Error "Failed at $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-PnPOnline -ErrorAction SilentlyContinue
}

$_.InvocationInfo.ScriptLineNumber tells you where it broke, which the message on its own does not. And the bare throw at the end re-raises the original error rather than swallowing it, so whatever called your script still finds out it failed.

Catching an error and then carrying on quietly is worse than not catching it. At least the red text was honest 🙂

PowerShell: Running Scripts Is Disabled on This System

You try to run a script and PowerShell refuses:

File C:\Scripts\demo.ps1 cannot be loaded because running scripts is disabled
on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.
    + CategoryInfo          : SecurityError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnauthorizedAccess

Or this one, which is a different problem with the same cause:

File C:\Scripts\demo.ps1 cannot be loaded. The file C:\Scripts\demo.ps1 is not
digitally signed. You cannot run this script on the current system.

Every answer you will find says to run Set-ExecutionPolicy. That is usually right and often does not work, because there are three separate things that produce these messages and only one of them is fixed that way.

First, what execution policy is not

It is not a security boundary. Microsoft say so themselves in the documentation. Anyone who can run PowerShell can bypass it in about four keystrokes, and I will show you how further down.

It exists to stop you double-clicking something by accident. Treat it as a seatbelt, not a lock.

The five scopes, and which one is winning

This is the part that catches people. There is not one execution policy, there are five, and they override each other in a fixed order:

MachinePolicy  ->  UserPolicy  ->  Process  ->  CurrentUser  ->  LocalMachine

Highest wins. So Get-ExecutionPolicy on its own tells you the effective answer but not which scope produced it. Always ask for the list:

Get-ExecutionPolicy -List

Here is my own machine:

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy       Undefined
   UserPolicy       Undefined
      Process          Bypass
  CurrentUser       Undefined
 LocalMachine    RemoteSigned

The effective policy is Bypass, not RemoteSigned. Something set it at Process scope for this session, and Process beats LocalMachine.

The important thing to note is that MachinePolicy and UserPolicy come from Group Policy. If either of those is set, nothing you type will change anything, and you will spend an afternoon finding out. Check those two first.

Fix 1 – set it for yourself only

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

-Scope CurrentUser does not need administrator rights, and it does not change the machine for anybody else. On a shared or managed machine that matters.

RemoteSigned is the sensible setting: scripts you wrote locally run, scripts that came from somewhere else need a signature. It is the default on Windows Server for that reason.

Fix 2 – bypass it for one command

If you cannot change the policy, or do not want to:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\demo.ps1

This changes nothing permanently. It applies to that one process and then it is gone.

I ran the same blocked script three ways to show the difference:

-ExecutionPolicy Restricted    "running scripts is disabled on this system"
-ExecutionPolicy AllSigned     "is not digitally signed"
-ExecutionPolicy Bypass        the script ran

Which is also the demonstration that execution policy is not security. Four extra words on the command line and the “blocked” script runs.

Fix 3 – the one nobody mentions

Here is the case that makes people think the policy is broken.

Your policy is RemoteSigned. You wrote a script locally, it runs fine. A colleague emails you one, or you download it, and it refuses – with the “not digitally signed” message rather than the “disabled on this system” one.

The policy is correct. The file is marked.

Windows adds a hidden alternate data stream to anything that arrives from the internet or an email client. You can see it:

Get-Item .\demo.ps1 -Stream *
Stream          Length
------          ------
:$DATA              30
Zone.Identifier     26

That Zone.Identifier stream is the mark of the web, and RemoteSigned is doing exactly what it says – treating the file as remote.

Remove it:

Unblock-File -Path .\demo.ps1

Same script, same policy, straight afterwards:

the script ran

Note that Unblock-File only removes the mark. It does not weaken your policy and it does not affect any other file. It is the right fix for this case, and changing the execution policy to work around it is the wrong one.

For a folder of scripts:

Get-ChildItem .\Scripts\*.ps1 -Recurse | Unblock-File

Fix 4 – you fixed the wrong PowerShell

This one cost me time, so it is worth knowing.

Windows PowerShell 5.1 and PowerShell 7 keep their execution policies in completely separate places. Setting it in one does nothing for the other.

They are different registry keys:

Windows PowerShell 5.1  HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
PowerShell 7 (Core)     HKLM\SOFTWARE\Microsoft\PowerShellCore\ShellIds\Microsoft.PowerShell

On my machine 5.1 has RemoteSigned set at both LocalMachine and CurrentUser, and the PowerShell 7 keys do not exist at all.

So if you opened the blue window, ran Set-ExecutionPolicy, and then went back to the black window and got the same error – that is why. Run $PSVersionTable.PSVersion in whichever one is refusing, and fix that one.

The order I would check them in

  1. Get-ExecutionPolicy -List – find which scope is actually winning
  2. If MachinePolicy or UserPolicy is set, stop. That is Group Policy and it is not your decision
  3. Get-Item .\script.ps1 -Stream * – if there is a Zone.Identifier, Unblock-File is your fix
  4. Check which PowerShell you are in
  5. Then, and only then, Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

Four of those five cost you nothing and change nothing. Reaching for Set-ExecutionPolicy first is how people end up with an unrestricted machine and a script that still does not run 🙂

Find a SharePoint List’s Internal Column Names with PowerShell

You write a script against a SharePoint list, ask for a column by the name you can see in the browser, and get nothing back. No error, just an empty value.

The column is called Project Status on screen. It is not called that underneath.

Run against a real list on PnP.PowerShell 2.12.0.

Every column has two names

The Title is the label in the browser. Anyone can change it and it can contain anything.

The InternalName is the key. It is set once, when the column is created, and it never changes again.

Here is the difference in one line:

$item = Get-PnPListItem -List 'Projects' -Id 1

$item['ProjectStatus']    # internal name
$item['Project Status']   # display name
By internal name 'ProjectStatus' : 'Complete'
By display name 'Project Status' : ''

An empty string, not an error. That is why this is so easy to lose an hour to – the script runs, it just quietly does nothing.

Getting the list

The obvious command gives you far more than you want:

Get-PnPField -List 'Projects'

On a list where I had created exactly five columns:

Total fields          : 90
Hidden                : 61
Inherited from base   : 83
Actually yours        : 5

Ninety. So filter it down to the ones you actually made:

Get-PnPField -List 'Projects' |
    Where-Object { -not $_.FromBaseType -and -not $_.Hidden } |
    Select-Object Title, InternalName, TypeAsString |
    Format-Table -AutoSize
Title          InternalName  TypeAsString
-----          ------------  ------------
Project Status ProjectStatus Choice
Project Owner  ProjectOwner  User
Due Date       DueDate       DateTime
Budget         Budget        Currency
Notes          Notes         Note

FromBaseType is the useful one. It is true for everything SharePoint gave you and false for everything you added.

What happens to spaces

Those internal names are clean because they were created by script. Columns created in the browser are not, because SharePoint derives the internal name from whatever you typed.

I made one with a space on purpose:

Add-PnPField -List 'Projects' -DisplayName 'Demo Review Date' `
             -InternalName 'Demo Review Date' -Type DateTime
Title        : Demo Review Date
InternalName : Demo_x0020_Review_x0020_Date
StaticName   : Demo_x0020_Review_x0020_Date

Every space becomes _x0020_. A column called “Date of Last Review” ends up as Date_x0020_of_x0020_Last_x0020_Review, and that is what your script has to use.

This is also why you should create columns with a short name and rename them afterwards, which brings us to the thing that actually catches people.

Renaming does not change the internal name

I renamed that column and looked again:

Set-PnPField -List 'Projects' -Identity 'Demo Review Date' -Values @{ Title = 'Renamed Review Date' }
Title        : Renamed Review Date
InternalName : Demo_x0020_Review_x0020_Date

The label changed. The key did not, and it never will.

So a column that says Review Date today might be Temp_x0020_Column underneath because of what somebody typed two years ago. You cannot work out the internal name by looking at the browser. You have to ask.

The practical version of that: create the column with the internal name you want, then rename it to whatever reads well. Give it ReviewDate first, then change the label to “Date of Last Review”. You get a clean key and a readable heading.

Built-in columns are worse

Some of the ones you use most have internal names with no relationship to the label at all:

Title        InternalName TypeAsString
-----        ------------ ------------
Attachments  Attachments  Attachments
Created By   Author       User
Content Type ContentType  Computed
Created      Created      DateTime
Modified By  Editor       User
Name         FileLeafRef  File
URL Path     FileRef      Lookup
ID           ID           Counter
Modified     Modified     DateTime
Title        Title        Text

Created By is Author. Modified By is Editor. Those two are the ones everybody hits, and there is nothing on screen that hints at it.

In a document library it gets stranger still:

Title        InternalName    TypeAsString
-----        ------------    ------------
Created By   Author          User
Type         DocIcon         Computed
Modified By  Editor          User
File Size    File_x0020_Size Lookup
Name         FileLeafRef     File
URL Path     FileRef         Lookup

A file’s Name is FileLeafRef. Its path is FileRef. And File Size carries the _x0020_ encoding as a built-in, which tells you Microsoft named it in a browser too.

One thing that will not work at all

I tried to make a column called Cost & Time-Frame to see what an ampersand does:

An error occurred while parsing EntityName. Line 1, position 32.

Not an encoded name – a failure. PnP builds field XML underneath, and a raw ampersand breaks the parse before SharePoint ever sees it. The error mentions XML parsing and does not mention your column, which is not helpful when you are three lines into a provisioning script.

Note that this is a script limitation. The browser will happily create that column and encode it for you. So this is one of the few cases where the UI does something PowerShell will not.

The line worth keeping

Get-PnPField -List 'YourList' |
    Where-Object { -not $_.FromBaseType -and -not $_.Hidden } |
    Select-Object Title, InternalName, TypeAsString |
    Sort-Object Title

Run it before you write anything against a list you did not create yourself. Thirty seconds, and it saves the empty-string hour 🙂