PowerShell -File vs. -Command: Which Exit Code Does the Caller Get?
Under -File the caller receives the exit code your script chose; under -Command, any script code other than 0 or 1 reaches the caller as 1.

On this page
TL;DR
Run your script with -File, not -Command. With -File, the launcher receives the number that the script passes to exit. The launcher can be Task Scheduler, a .cmd file or another PowerShell session. With -Command, the launcher receives 1 for any number other than 0 or 1. Under -Command, a script that ends with exit 3 reports 1. Task Scheduler shows 1 as a Last Run Result of 0x1.
powershell.exe -NoProfile -File "C:\Scripts\job.ps1"With either parameter, a script reports failure only when it exits with a number other than 0. A script still exits 0 after a Write-Error that lets it carry on. A script also exits 0 when its last line is a native command that fails. To pass a native command’s code on under -File, end the script with exit $LASTEXITCODE. Under -Command, end the command string with ; exit $LASTEXITCODE.
I call whatever starts your script the launcher: Task Scheduler, a .cmd file, or another PowerShell session. I ran every case in this article on Windows Server 2025, in Windows PowerShell 5.1 and PowerShell 7. The cases in the first two tables, the command strings and the missing script ran from three launchers: a .cmd file read %ERRORLEVEL%, a PowerShell session read $LASTEXITCODE, and a scheduled task showed its Last Run Result. All three got the same number every time, and Task Scheduler shows it in hex. The rest ran as scheduled tasks only. Microsoft documents the switches in about_PowerShell_exe (opens in a new tab) and about_Pwsh (opens in a new tab). The tables here show what my runs returned.
Exit codes under -File
Under -File, the launcher gets the code your script exits with. Windows PowerShell 5.1 and PowerShell 7 give the same results:
| Script | Exit code under -File |
|---|---|
'ok' | 0 |
exit 3 | 3 |
exit 256 | 256 (0x100 in Task Scheduler) |
exit -1 | -1 (0xFFFFFFFF in Task Scheduler) |
throw 'boom' | 1 |
A syntax error ('a' +) | 1 |
Write-Error 'boom', then 'after' | 0 |
'before', then Write-Error 'boom' as the last statement | 0 |
cmd.exe /c exit 5 as the last line | 0 |
A script-terminating error is one that stops your script on the spot, like throw or a cmdlet run with -ErrorAction Stop. If your script stops on one, the launcher gets 1. A non-terminating error is one your script reports and then carries on past, like Write-Error. If your script hits only errors like that, the launcher gets 0. A native command is a program that isn’t PowerShell, like cmd.exe or robocopy. If your script ends on one, the launcher also gets 0. So does a script that runs Import-Module on a missing module and then calls a command that doesn’t exist. I saw that one in an earlier run as SYSTEM.
Exit codes under -Command
Under -Command "& 'script.ps1'", the launcher doesn’t get your script’s code. It gets 0 or 1:
| Script | Under -File | Under -Command "& 'script.ps1'" |
|---|---|---|
exit 3 | 3 | 1 |
exit 256 | 256 | 1 |
exit -1 | -1 | 1 |
ok, throw, Write-Error, a syntax error, a native command | as in the earlier table | the same as under -File |
When your script runs, the two switches differ only where it exits with a code other than 0 or 1. That’s why your task shows 0x1 for a script that ends with exit 3.
An exit inside the command string keeps its code. With no script file, -Command "exit 5" gives 5 in both editions. Microsoft documents the same change to 1 when a native command is the last command in the string. I didn’t test that case.
So where does the 0 or 1 come from? PowerShell keeps a success flag, $?, which reads True or False for the statement it ran last. Under -Command, PowerShell runs your string and then reports that flag: 0 for True, 1 for False. When the last statement is & 'script.ps1', the flag depends on how your script ended:
- If your script exits with any code other than 0,
$?isFalse, and the launcher gets 1. - If your script only wrote a non-terminating error, or ended on a native command,
$?staysTrue, and the launcher gets 0.
That’s why the same non-terminating error gives different codes, depending on what runs last in the string:
| Command string | Exit code |
|---|---|
"Write-Error 'boom'" | 1 |
"Write-Error 'boom'; 'after'" | 0 |
"& 'script.ps1'", where the script’s last statement is Write-Error 'boom' | 0 |
"throw 'boom'" | 1 |
Keep the real code under -Command
Pass the code on yourself: end the command string with exit $LASTEXITCODE.
-NoProfile -Command "& 'C:\Scripts\job.ps1'; exit $LASTEXITCODE"This text goes in the task’s Arguments field or on a line in a .cmd file. If you build it inside a double-quoted PowerShell string, for example for New-ScheduledTaskAction -Argument, escape the dollar sign as `$LASTEXITCODE. Otherwise your session fills in $LASTEXITCODE itself before the new PowerShell starts. If your script ends with exit 5, your task now gets 5 in both editions. Without this ending, it gets 1. When I redirected the call’s output, this form returned 0 for a missing script and for a script that doesn’t parse. I didn’t test it without the redirect. If your script might not run, use the try/catch form in “Script that never ran returns 0 with exit $LASTEXITCODE”. Keep the -Command form when you need the redirect wrapper that Task Scheduler Falsely Reports Success for a PowerShell Script That Works Manually describes.
Pass a native command’s code through
A native command’s exit code doesn’t become your script’s exit code. If your script ends with cmd.exe /c exit 5, it returns 0 under both switches. To pass the code on, end your script with exit $LASTEXITCODE:
cmd.exe /c exit 5
exit $LASTEXITCODEUnder -File, the launcher now gets 5 in both editions. Under -Command, the same script still gives 1. It gives 5 once the command string ends with ; exit $LASTEXITCODE, whether or not your script has its own exit line. A native command or a script’s exit sets $LASTEXITCODE. For why a failed cmdlet leaves it alone, see Why $LASTEXITCODE Doesn’t Work the Way You Expect in PowerShell.
Read the exit code from each launcher
Each launcher prints the same number in a different form. Run your script from each one to confirm what you get.
In cmd.exe, %ERRORLEVEL% holds the code in decimal:
powershell.exe -NoProfile -File C:\Scripts\job.ps1
echo %ERRORLEVEL%In the PowerShell session that starts your script, $LASTEXITCODE holds it in decimal:
& powershell.exe -NoProfile -File C:\Scripts\job.ps1
$LASTEXITCODEIn Task Scheduler, Get-ScheduledTaskInfo prints LastTaskResult as an unsigned decimal, so exit -1 reads 4294967295 there. Last Run Result shows the same number in hex, 0xFFFFFFFF. This command prints the hex form:
Get-ScheduledTaskInfo -TaskName '<task-name>' -TaskPath '\' |
Select-Object LastRunTime, @{ Name = 'LastTaskResult'; Expression = { '0x{0:X}' -f $_.LastTaskResult } }The task history records the code as an HRESULT, a Windows error number. For exit 1 it shows return code 2147942401 (0x80070001), in an entry that still reads “successfully completed”. Do not read that entry as success. Task history was off on the fresh Windows Server 2025 Azure Marketplace image I tested. Turn it on with wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true before the run you want to see.
Script called inside a session
When you call a script with & inside a PowerShell session, the script’s exit doesn’t end your session. In both editions, exit 3 in the called script ends only that script, and your session carries on. After the call, $LASTEXITCODE and $? read like this:
| Called script | $LASTEXITCODE | $? |
|---|---|---|
'ok' | unset | True |
exit 3 | 3 | False |
throw 'boom' | unset | not read; the caller’s catch ran with boom |
Write-Error 'boom' | unset | True |
cmd.exe /c exit 5 | 5 | True |
I reset $LASTEXITCODE to $null before each call. After a script that ends in cmd.exe /c exit 5, $? is True for the script call. Read $LASTEXITCODE, not $?, to get the native command’s result.
Check your scripts with a coding agent
Paste this into your own local coding agent. It reads your scheduled tasks and reports which ones can lose an exit code. Add the names of any CI steps or .cmd wrappers you want checked on the line that starts “Additional places to check”. It changes nothing.
You are auditing how PowerShell scripts are launched. Read only. Do not change
anything: no Set-ScheduledTask, no Register-ScheduledTask, no edits to any
script, task, wrapper or CI file. Do not start any task and do not run any
script.
Additional places to check (leave empty if none): <CI steps or .cmd wrappers>
1. List every scheduled task whose action runs powershell.exe or pwsh.exe, with
Get-ScheduledTask. Add every item named above whose command line runs
powershell.exe or pwsh.exe. For each, report the task name or file, the
executable, and the arguments verbatim.
2. Classify each by how the script is launched:
- File: the arguments use -File.
- CommandAmpersand: the arguments use -Command with & 'script' .
- CommandInline: the arguments use -Command with a command string that does
not call a script file.
- Other: a wrapper that launches powershell, or anything not covered by the
rules below. Report the arguments verbatim and say the result was not
measured.
For pwsh.exe, treat -f and -c as -File and -Command (documented aliases in
about_Pwsh). Report any other abbreviation, and any abbreviation on
powershell.exe, verbatim as Other. With neither switch, pwsh.exe arguments are File (about_Pwsh);
for powershell.exe, report the arguments verbatim and say the result was not
measured. Decode -EncodedCommand, then classify the decoded string as
CommandAmpersand or CommandInline.
3. For each, say whether an exit code other than 0 or 1 from the script would
reach the caller, using these measured rules:
- File: the script's exit code reaches the caller unchanged, if the path
is right (see the path rule below).
- CommandAmpersand: any code other than 0 or 1 becomes 1, unless the command
string ends with "; exit $LASTEXITCODE". If it does end that way, the code
survives. Measured with the script's output redirected (*> log): a script
path that does not exist or a script that does not parse then returns 0,
unless the call sits inside try { } catch { exit 1 }. Without a redirect
this case was not measured; say so.
- CommandInline: "exit N" inside the string keeps N. A string that is only
Write-Error gives 1; Write-Error followed by a statement that succeeds
(measured: Write-Error 'boom'; 'after') gives 0. The code follows the
last statement's status. For any other string, say the result was not
measured.
- File and CommandAmpersand: a script that ends on a native command returns
0. Under File, ending the script with "exit $LASTEXITCODE" passes the code
on. Under CommandAmpersand, the command string must also end with
"; exit $LASTEXITCODE". A Write-Error that does not stop the script leaves
the code at 0.
- File: if the script path in the arguments is unquoted and contains a space,
or the file does not exist, the code is 0xFFFD0000 (Windows PowerShell 5.1)
or 0x40 (PowerShell 7) and no script code survives. Check the path exists
and is quoted.
For items from the Additional places line, say the caller was not measured
(the base File and CommandAmpersand results were measured for cmd.exe, a
parent PowerShell and Task Scheduler; the "; exit $LASTEXITCODE", redirect
and try/catch results for Task Scheduler only) and report the
classification only.
4. For each File and CommandAmpersand task, open the script it names (read only) and report whether
it ends with an explicit exit, and whether its last statement is a native
command.
Report one row per task: name, launch type, whether a code other than 0 or 1
survives, and why. Then propose each fix as text for me to review and apply.Common failures
Exit code -196608 (0xFFFD0000) or 64: missing script or unquoted path with a space
When your script doesn’t exist, the code depends on the edition and the switch. An unquoted path that contains a space, such as -File C:\ts\My Scripts\ok.ps1, gives the same two codes:
| Action | Windows PowerShell 5.1 | PowerShell 7 |
|---|---|---|
-File with a missing script | -196608 (0xFFFD0000) | 64 (0x40) |
-File with an unquoted path that contains a space | 0xFFFD0000 | 0x40 |
-Command "& 'script.ps1'" with a missing script | 1 | 1 |
cmd.exe and a PowerShell session print -196608 for the first value. Get-ScheduledTaskInfo prints 4294770688, and Task Scheduler prints 0xFFFD0000. To fix the space case, quote the path: -File "C:\ts\My Scripts\ok.ps1" gives 0 in both editions.
Script that never ran returns 0 with exit $LASTEXITCODE
I ran the wrapper -Command "& 'script.ps1' *> log; exit $LASTEXITCODE", which redirects the output to a log. It returned 0 for a missing script and for a script with a syntax error, in both editions, because no native command set the code. The form in “Keep the real code under -Command” has no redirect, and I didn’t test this case for it. Wrap the call in try and write the error to the log, and the launcher gets 1 for both:
-NoProfile -Command "try { & 'C:\Scripts\job.ps1' *> C:\Scripts\logs\job.log } catch { $_ *>> C:\Scripts\logs\job.log; exit 1 }; exit $LASTEXITCODE"The same escaping applies as earlier: inside a double-quoted PowerShell string, escape each $ with a backtick. For how to write that log, see Task Scheduler Falsely Reports Success for a PowerShell Script That Works Manually.
What these tests did not cover
- A CI runner as the launcher. I tested cmd.exe, a PowerShell session, and Task Scheduler.
returninstead ofexit, and-Filewith arguments passed to the script.- Windows Server 2022, Windows 11, and Task Scheduler logons other than a SYSTEM task.
Tested with
- OS
- Windows Server 2025 Datacenter 24H2, build 26100.33674
- PowerShell
- Windows PowerShell 5.1.26100.33648 and PowerShell 7.6.9
- Launchers
- cmd.exe, a parent Windows PowerShell 5.1 session, and a Task Scheduler task running as SYSTEM
- Earlier run
- Build 26100.33650 with PowerShell 7.6.8, Task Scheduler only
- Not tested
- CI runners, return, Windows Server 2022, Windows 11
- Date
- October 2026