Votre script PowerShell lance une commande longue, attend qu'elle se termine, contrôle le code de retour et s'arrête sur un message incompréhensible :
Il n'y a rien après le mot « code ». Pourtant, la commande a bien fonctionné. Sur la migration de poste qui nous a fait tomber sur ce problème, en novembre 2026, la distribution Linux était présente en version 2, la sortie annonçait une opération réussie et le fichier d'erreurs du script était vide.
La commande n'a rendu aucun code anormal : PowerShell a perdu le code. Avec certains paramètres de Start-Process, l'objet renvoyé par -PassThru ne peut plus lire ExitCode une fois le processus terminé. La propriété reste vide, et le test classique -ne 0 prend ce vide pour une erreur. La correction tient en une ligne, à condition de la placer au bon endroit.
wsl de cet article portent sur des distributions : adaptez les noms et les chemins à votre cas.
| Situation | Ce qu'il faut faire |
|---|---|
ExitCode vide après -PassThru | Ajouter $null = $p.Handle juste après Start-Process |
| Vous n'avez pas besoin de faire autre chose pendant l'exécution | Appeler la commande avec & et lire $LASTEXITCODE |
| Le script vient de s'arrêter sur un code vide | Contrôler le résultat réel, ne rien relancer, ne rien supprimer |
$p.WaitForExit() n'a rien changé | C'est normal : seul, il ne conserve pas le code |
Vous testez -not $p.ExitCode | Danger : un vrai échec passe pour un succès, voir la section 3 |
Ouvrez Windows PowerShell et collez ceci. La commande rend volontairement le code 7.
$p = Start-Process cmd.exe -ArgumentList '/c', 'exit 7' -PassThru -NoNewWindow Start-Sleep -Seconds 1 "Code : [$($p.ExitCode)]"
Vous obtenez Code : []. Remplacez -NoNewWindow par -WindowStyle Hidden : vous obtenez Code : [7].
Tout est là. Le problème ne vient ni de -PassThru, ni de la commande lancée. Il vient de la manière dont Start-Process crée le processus.
Sous Windows, le code de retour d'un processus terminé reste lisible tant qu'un programme garde un handle sur ce processus, c'est-à-dire une référence ouverte vers lui. Quand plus personne n'en détient, le code disparaît avec lui.
Start-Process a deux façons de démarrer un programme :
Start de .NET. L'objet renvoyé a créé le processus lui-même et conserve son handle.Process.GetProcessById. Il n'a donc jamais démarré ce processus.Le code de Windows PowerShell 5.1 n'est pas public, mais il se comporte de la même façon. Voici ce que nous avons mesuré sur la version 5.1.26100.9818, avec une commande rendant le code 7 et une attente sur HasExited :
Paramètres de Start-Process avec -PassThru | ExitCode lu ensuite |
|---|---|
-WindowStyle Hidden | 7 |
-WorkingDirectory seul | 7 |
-NoNewWindow | vide |
-RedirectStandardOutput, -RedirectStandardError | vide |
-RedirectStandardError et -WindowStyle Hidden | vide |
-UseNewEnvironment | vide |
-LoadUserProfile | vide |
Masquer la fenêtre ne protège donc de rien : une simple redirection suffit à perdre le code. C'est le montage le plus courant dans un script d'administration, puisqu'on veut justement garder une trace des erreurs dans un fichier.
Pourquoi aucune erreur ne s'affiche. .NET lève bien une exception, mais PowerShell l'avale quand on lit une propriété, et il renvoie $null. Set-StrictMode -Version Latest n'y change rien. Pour la voir, appelez directement la méthode cachée derrière la propriété :
$p.get_ExitCode()
Sur un Windows en français, le message est explicite : « Le processus n'ayant pas été démarré par cet objet, les informations demandées ne peuvent pas être déterminées. »
Dans PowerShell, $null n'est pas égal à 0. Le test $null -ne 0 est donc vrai. Le script conclut à une erreur alors que le code n'a simplement jamais été lu.
Ce serait presque rassurant si le défaut allait toujours dans ce sens. Or tout dépend de la façon d'écrire le test :
| Test écrit dans le script | Commande réussie, code perdu | Commande échouée, code perdu |
|---|---|---|
if ($p.ExitCode -ne 0) { échec } | Échec, à tort | Échec, par chance |
if ($p.ExitCode -eq 0) { succès } | Échec, à tort | Échec, par chance |
if (-not $p.ExitCode) { succès } | Succès, par chance | Succès, à tort |
if ([int]$p.ExitCode -eq 0) { succès } | Succès, par chance | Succès, à tort |
Les deux dernières lignes sont les plus dangereuses. La commande a échoué, le script continue, et rien ne le signale. [int]$null vaut en effet 0.
Les variables automatiques ne vous sauveront pas non plus. Après un Start-Process dont le programme a rendu le code 7, $? reste à True, car la commande PowerShell a bien lancé le programme. Quant à $LASTEXITCODE, il n'est pas mis à jour : dans notre essai, il gardait la valeur 3 de la commande précédente.
Ne relancez pas l'étape. Si elle a réussi, vous allez au mieux obtenir une erreur, au pire faire le travail une seconde fois par-dessus le premier.
wsl --list --verbose et vérifiez que la distribution apparaît, avec la bonne version. Pour une copie, comparez les fichiers. Pour une installation, lancez le programme.N'essayez surtout pas de repartir de zéro avec wsl --unregister. Microsoft prévient que toutes les données, les réglages et les logiciels de la distribution sont alors perdus définitivement. Sur cette migration, l'importation avait réussi : cette commande aurait détruit un travail terminé, pour corriger une erreur qui n'existait pas.
| Méthode | Quand la choisir | Sa limite |
|---|---|---|
& et $LASTEXITCODE | Le script attend la commande sans rien faire d'autre | Aucun suivi pendant l'exécution |
-Wait -PassThru | Vous gardez Start-Process sans rien faire d'autre | Attend aussi les processus enfants |
$null = $p.Handle | Vous voulez afficher un suivi ou imposer un délai | À placer tout de suite, sinon sans effet |
Solution 1 : l'appel direct, à préférer quand c'est possible. Microsoft le rappelle dans sa documentation : Start-Process ne devrait servir que lorsque vous avez besoin de contrôler la façon dont la commande s'exécute.
& wsl.exe --import Debian C:\WSL\Debian C:\Sauvegarde\debian.tar --version 2
if ($LASTEXITCODE -ne 0) { throw "wsl --import a rendu le code $LASTEXITCODE" }
Un piège sous PowerShell 5.1 : n'ajoutez pas 2>&1 si le script tourne avec $ErrorActionPreference = 'Stop'. Dans notre essai, une commande qui écrivait une ligne sur sa sortie d'erreur a déclenché une exception alors qu'elle rendait le code 0. C'est exactement le même faux échec, par un autre chemin.
Solution 2 : -Wait. Sur 5.1, -Wait -PassThru rend bien le code, même avec des redirections. Sa contrepartie est documentée : -Wait attend tout l'arbre de processus, descendants compris. Avec une commande qui laissait tourner un programme en arrière-plan, notre script a attendu 6,1 secondes, contre un retour immédiat avec la solution 3. Une commande qui laisse un service derrière elle peut ainsi bloquer votre script.
Solution 3 : conserver le handle. Il suffit de lire la propriété Handle pour que l'objet le garde en mémoire.
$p = Start-Process wsl.exe -ArgumentList '--status' -PassThru -NoNewWindow `
-RedirectStandardOutput C:\Temp\sortie.txt -RedirectStandardError C:\Temp\erreurs.txt
$null = $p.Handle
$p.WaitForExit()
$p.ExitCode
Deux précisions, vérifiées chacune par un essai :
Start-Process. Lu deux secondes plus tard, sur un processus déjà terminé, $p.Handle ne produit aucune erreur, et ExitCode reste vide.$p.WaitForExit() seul ne suffit pas. Sans la ligne Handle, le code reste vide. Wait-Process a rendu le code tant que le processus tournait encore, mais il échoue s'il est déjà terminé : ne comptez pas dessus.Cette fonction regroupe les bonnes pratiques : journaux séparés, handle conservé, suivi toutes les 30 secondes et refus de conclure si le code reste introuvable. Elle s'est comportée correctement dans nos essais, avec les codes 0, 5, 7 et -1, sur une commande de 35 secondes et sur de vraies commandes wsl.exe.
function Lancer-Commande {
param(
[Parameter(Mandatory)][string]$Programme,
[string[]]$Arguments,
[Parameter(Mandatory)][string]$Journal
)
$p = Start-Process -FilePath $Programme -ArgumentList $Arguments -PassThru -NoNewWindow `
-RedirectStandardOutput "$Journal.sortie.txt" -RedirectStandardError "$Journal.erreurs.txt"
$null = $p.Handle
while (-not $p.WaitForExit(30000)) {
Write-Host "En cours depuis $([int]((Get-Date) - $p.StartTime).TotalMinutes) min"
}
if ($null -eq $p.ExitCode) { throw "Code de retour indisponible pour $Programme" }
return $p.ExitCode
}
Le test $null -eq $p.ExitCode est la vraie garantie : un code perdu arrête le script avec un message clair, au lieu de se faire passer pour un échec ou pour un succès.
Le dossier du journal doit exister avant l'appel. Si un chemin passé dans $Arguments contient une espace, ajoutez-lui des guillemets, car Start-Process assemble les arguments en une seule ligne.
Ajoutez ensuite un contrôle du résultat réel. Le code dit ce que la commande pense avoir fait, pas ce qui existe :
$env:WSL_UTF8 = '1'
$code = Lancer-Commande -Programme wsl.exe -Journal C:\Temp\wsl-import -Arguments `
'--import', 'Debian', 'C:\WSL\Debian', 'C:\Sauvegarde\debian.tar', '--version', '2'
$presente = (wsl.exe --list --quiet) -contains 'Debian'
if ($code -ne 0 -or -not $presente) { throw "Import non confirmé : code $code, présente : $presente" }
N'oubliez pas la ligne WSL_UTF8. Sous PowerShell 5.1, wsl.exe sort son texte en UTF-16, et chaque lettre capturée est suivie d'un caractère nul. Dans notre essai, sans cette variable, -match, -like et Select-String ne trouvaient pas une distribution pourtant installée, et seul -contains répondait juste. Votre vérification peut donc se tromper à son tour. Avec WSL_UTF8=1, tous ces tests répondent juste, et les journaux redirigés passent eux aussi de l'UTF-16 à l'UTF-8. Sans elle, relisez-les avec Get-Content -Encoding Unicode.
Une fois le code fiable, reste à l'interpréter. Le zéro pour un succès n'est qu'une convention : Microsoft rappelle que chaque programme fixe la sienne.
| Commande | Code | Ce qu'il veut dire |
|---|---|---|
dism /enable-feature | 3010 | Terminé, un redémarrage est nécessaire |
robocopy | 1 | Des fichiers ont été copiés sans erreur, voir notre article sur robocopy |
tar -xf archive -C C:\ | 1 | Cas vu sur la même migration : l'archive contenait une entrée ./, tous les fichiers étaient pourtant posés |
wsl --terminate sur un nom inconnu | -1 | Vrai échec, ici une distribution introuvable |
Le cas de tar montre bien la règle : lisez la documentation de l'outil, et contrôlez le résultat. Sur cette migration, c'est la taille de chaque fichier qui a tranché, pas le code.
Le défaut est signalé depuis la bêta de PowerShell 6 dans le dépôt officiel. Il concernait précisément -NoNewWindow et les paramètres -RedirectStandard*, et uniquement Windows. PowerShell 7.4.0 l'avait même étendu au cas de -Wait.
Une correction a été intégrée fin 2023 pour la version 7.4.1, et l'auteur du signalement l'a confirmée sur la 7.4.2. Le code actuel lit désormais process.Handle juste après la création du processus. Nous n'avons pas essayé PowerShell 7 sur notre poste : ce point repose sur le dépôt GitHub.
En pratique, powershell.exe reste la version 5.1 livrée avec Windows, et pwsh.exe la version 7 installée à part. Sur la 5.1 de notre poste, en novembre 2026, le défaut existe toujours. Si votre script doit tourner sur les deux, gardez la ligne $null = $p.Handle : elle se contente de lire une propriété.
Une question sur votre script ? Écrivez-nous, avec la ligne Start-Process utilisée et le résultat de $PSVersionTable.PSVersion.
$p.ExitCode est-il vide après Start-Process -PassThru ?Avec -NoNewWindow ou une redirection, l'objet renvoyé ne détient pas de handle sur le processus. Une fois celui-ci terminé, le code n'est plus lisible, et PowerShell masque l'erreur en renvoyant $null.
Start-Process ?Tapez $null = $p.Handle juste après Start-Process, puis lisez $p.ExitCode une fois le processus terminé. Vous pouvez aussi utiliser -Wait -PassThru.
Start-Process met-il à jour $LASTEXITCODE ?Non, dans notre essai la variable a gardé sa valeur précédente. Elle n'est alimentée que si vous lancez le programme directement, par exemple avec &.
Set-StrictMode signale-t-il le problème ?Non. Même en -Version Latest, la propriété renvoie un vide sans erreur. Seul $p.get_ExitCode() montre l'exception.
wsl --import a réussi ?Lisez son code avec & et $LASTEXITCODE, puis vérifiez que la distribution apparaît dans wsl --list --verbose. Dans un script, réglez $env:WSL_UTF8 = '1' avant d'analyser cette liste.
D'après le dépôt officiel, il est corrigé depuis la branche 7.4.1. Il subsiste sous Windows PowerShell 5.1, où nous l'avons reproduit en novembre 2026.
BSC Informatique, EURL BSC INFORMATIQUE, 18 Avenue Olbius Riquier, 83400 Hyères, 04 94 27 65 95.
Windows, migrations et postes de travail,
pour les particuliers et les professionnels.