Gestion des erreurs et conception des nouvelles tentatives sous PowerShell — du piège du try/catch aux bonnes pratiques d'exit code et de retry

· · PowerShell, Windows, Gestion des erreurs, Nouvelles tentatives, Automatisation, Amélioration opérationnelle, Script, Planificateur de tâches

« Le traitement de nuit avait échoué, mais le Planificateur de tâches affichait pourtant un succès (0x0), et personne ne s’en est aperçu » — « j’ai écrit un try/catch, mais l’exécution n’entre jamais dans le bloc catch » — « le script tombe une fois par mois à cause d’une coupure réseau momentanée ». Dès qu’un script PowerShell passe en production, ce genre de remarque finit toujours par arriver. Entre un script qui satisfait quand on le lance à la main et un script qui tourne seul, chaque nuit, sans supervision, se dresse un mur : la conception de la gestion des erreurs et des nouvelles tentatives (retry).

Ce qui complique les choses, c’est que le modèle d’erreurs de PowerShell diffère subtilement du modèle d’exceptions des langages de programmation habituels. « Une erreur s’est produite, mais le traitement continue » ou « je pensais l’avoir capturée, mais elle m’a filé entre les doigts » ne sont, dans la plupart des cas, pas des bugs : c’est PowerShell qui se comporte exactement comme prévu par sa spécification. Écrire du code sans comprendre ce mécanisme conduit à produire en série des scripts qui avalent leurs échecs et se terminent comme si tout s’était bien passé.

Cet article s’adresse aux responsables informatiques et développeurs qui automatisent des tâches internes récurrentes avec PowerShell. En s’appuyant sur la documentation officielle, il détaille la distinction entre erreurs terminales et non terminales, la manière de juger du succès d’une commande native, la conception d’exit codes permettant au Planificateur de tâches et à la supervision de distinguer succès et échec, ainsi qu’un modèle de nouvelle tentative capable d’absorber les erreurs temporaires.

1. L’essentiel en bref

  • Les erreurs PowerShell se répartissent entre « erreurs non terminales » et « erreurs terminales » (de fin d’instruction ou de fin de script). Une erreur non terminale affiche un message et laisse le pipeline continuer ; par défaut, elle n’entre pas dans un try/catch.1
  • La pratique standard consiste à ajouter -ErrorAction Stop à toute commande que l’on souhaite voir capturée par try/catch. Stop promeut une erreur non terminale en erreur terminale, que catch peut alors traiter. On peut aussi définir $ErrorActionPreference (Continue par défaut) sur Stop en début de script.12
  • -ErrorAction remplace $ErrorActionPreference pour cette seule commande. Les deux ne sont toutefois pas parfaitement symétriques : -ErrorAction ne contrôle que les erreurs non terminales.1
  • L’échec d’une commande native (robocopy, git, un EXE externe) ne devient pas, par défaut, une erreur PowerShell. Un code de sortie non nul fait passer $? à $false et alimente $LASTEXITCODE, mais aucun ErrorRecord n’est créé et rien n’entre dans catch. Jugez du succès avec $LASTEXITCODE.1
  • Dans PowerShell 7.4, $PSNativeCommandUseErrorActionPreference est devenu une fonctionnalité officielle. En le passant à $true, un code de sortie non nul déclenche une erreur non terminale ; combiné à $ErrorActionPreference = 'Stop', try/catch peut alors la capturer (la valeur par défaut est $false).32
  • Dans un bloc catch, $_ est l’ErrorRecord. $_.Exception donne l’exception elle-même, et pour une erreur promue, $_.Exception.ErrorRecord permet de remonter à l’information d’erreur d’origine. Un bloc catch typé permet de traiter individuellement les seules erreurs attendues.14
  • Le succès ou l’échec doit toujours être communiqué à l’extérieur via l’exit code. Le mot-clé exit fixe le code de sortie ; si le script est lancé avec pwsh -File / powershell.exe -File, cette valeur devient le code de sortie du processus. Sans exit, une fin normale vaut 0 et une exception non gérée vaut 1.56
  • Les nouvelles tentatives reposent sur trois principes : limitées aux erreurs temporaires, plafonnées, et idempotentes. Ne pas maquiller une erreur métier par une nouvelle tentative, élargir l’intervalle avec un backoff exponentiel, et concevoir le traitement pour qu’une réexécution ne provoque jamais de double traitement. Ce n’est qu’en réunissant ces trois conditions que l’on obtient un script réellement sûr à relancer.

2. Deux types d’erreurs — pourquoi le try/catch ne fonctionne pas

Les erreurs PowerShell se répartissent en trois catégories : les erreurs non terminales (elles se contentent de signaler le problème sans arrêter le pipeline), les erreurs de fin d’instruction (elles n’arrêtent que cette instruction et passent à la suivante), et les erreurs de fin de script (elles déroulent toute la pile d’appels).1

Sur le terrain, le piège vient des erreurs non terminales. Quand une applet de commande comme Get-Content ou Get-ChildItem échoue à traiter une entrée particulière, ce qu’elle génère est en général une erreur non terminale : un message d’erreur en rouge s’affiche, mais le traitement continue, sans jamais entrer ni dans try/catch ni dans trap.1

# [Piège] catch n'est jamais atteint, et "Terminé" s'affiche quand même
try {
    Get-Content -Path 'C:\Data\fichier-inexistant.txt'   # erreur non terminale
    Write-Host 'Terminé'                                  # s'exécute quand même
}
catch {
    Write-Host 'On ne passe jamais ici'
}

# [Pratique standard] -ErrorAction Stop promeut l'erreur en erreur terminale afin que catch puisse la traiter
try {
    Get-Content -Path 'C:\Data\fichier-inexistant.txt' -ErrorAction Stop
    Write-Host 'Terminé'                                  # ignoré en cas d'erreur
}
catch {
    Write-Host "Erreur capturée : $($_.Exception.Message)"
}

Lorsque -ErrorAction Stop ou $ErrorActionPreference = 'Stop' est actif, le moteur enveloppe l’erreur non terminale dans une ActionPreferenceStopException et la promeut en erreur terminale. À l’intérieur d’un bloc try, c’est cette erreur promue qui arrive jusqu’à catch — voilà le mécanisme exact à l’œuvre.1 En revanche, les exceptions issues de méthodes .NET (comme [int]::Parse('abc')) ou l’échec de résolution d’un nom de commande sont des erreurs terminales dès le départ, elles atteignent donc catch sans effort supplémentaire.1

L’idée « alors pourquoi ne pas toujours mettre $ErrorActionPreference = 'Stop' » est à moitié juste. Pour un script exécuté sans supervision, s’arrêter et signaler l’échec est plus sûr que d’avaler l’erreur et de continuer, donc définir Stop en tête de script est un bon réglage par défaut. Gardez toutefois à l’esprit que $ErrorActionPreference s’applique à sa portée et à ses portées enfants, ce qui modifie aussi le comportement des modules ou fonctions appelés, et que les étapes de nettoyage où l’échec est acceptable (suppression de fichiers temporaires, par exemple) doivent se voir réattribuer explicitement -ErrorAction SilentlyContinue.2

3. Que lire dans un bloc catch — explorer l’ErrorRecord

Dans un bloc catch, $_ contient l’ErrorRecord. Toutes les informations à consigner dans les journaux en proviennent.14

try {
    Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
    # Un catch typé traite individuellement uniquement les "échecs attendus".
    # Même pour une erreur promue, le moteur effectue la correspondance sur le type d'exception d'origine
    Write-Warning "Erreur d'E/S : $($_.Exception.Message)"
}
catch {
    # Les erreurs inattendues sont journalisées avec leur contexte puis relancées (ne pas les avaler)
    $rec = $_   # $_ est l'ErrorRecord
    Write-Warning ('Type : {0} / Emplacement : {1} / Cible : {2}' -f `
        $rec.Exception.GetType().FullName,
        $rec.InvocationInfo.PositionMessage,
        $rec.TargetObject)
    throw       # un throw sans argument propage la même erreur vers le niveau supérieur
}
finally {
    # finally s'exécute que try réussisse, échoue, ou soit interrompu par Ctrl+C. Placez le nettoyage ici
    if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}

Trois points méritent votre attention.

  • $_.Exception est l’exception elle-même. Une erreur promue par -ErrorAction Stop est enveloppée dans une ActionPreferenceStopException, mais lorsque catch effectue sa correspondance de type, le moteur regarde le type d’exception d’origine (par exemple ItemNotFoundException) : on peut donc écrire un catch typé exactement comme d’habitude. On peut remonter jusqu’à l’ErrorRecord d’origine via $_.Exception.ErrorRecord.1
  • $_.InvocationInfo.PositionMessage indique précisément dans quel fichier, à quelle ligne et sur quelle commande l’erreur s’est produite ; sa présence ou son absence dans les journaux d’une exécution sans supervision peut faire une différence d’un ordre de grandeur sur le temps d’investigation.
  • Le bloc finally s’exécute que le try réussisse, échoue en erreur, ou soit arrêté par Ctrl+C. Le nettoyage — fermeture de connexions, suppression de fichiers temporaires — doit être placé dans finally.7

La question de conception — à quel niveau capturer et où consigner les journaux — est universelle, quel que soit le langage. Les principes exposés dans « Où placer catch et la journalisation dans la gestion des exceptions ? » (capturer à la frontière, ne pas avaler les erreurs, éviter la double journalisation) s’appliquent tels quels à PowerShell.

4. Succès ou échec d’une commande native — $?, $LASTEXITCODE et la nouveauté de la 7.4

Un autre grand angle mort concerne les commandes natives — robocopy, git, ou un EXE interne à l’entreprise. Les programmes externes ne participent pas au système d’erreurs de PowerShell ; ils signalent leurs échecs via un code de sortie. Voici le comportement par défaut.1

Événement Comportement (par défaut)
Code de sortie non nul $? passe à $false, et le code de sortie est placé dans $LASTEXITCODE
Création d’un ErrorRecord Aucune (pas même ajouté à $Error)
try/catch Non déclenché

Autrement dit, try { robocopy ... } catch { ... } ne capture rien (par défaut). Le test de succès d’une commande native doit donc s’écrire avec $LASTEXITCODE. $? est une valeur booléenne indiquant si « l’opération précédente a réussi », et pour une commande native, elle ne vaut $true que lorsque le code de sortie est 0.1 Notez aussi que dans Windows PowerShell 5.1, il suffisait qu’une commande native écrive sur stderr pour que $? passe à $false, mais PowerShell 7 a modifié ce comportement pour que $? ne devienne $false que sur un code de sortie non nul — un changement plus conforme à la réalité, puisqu’écrire sur stderr n’équivaut pas à un échec.8

# Une commande native se juge avec $LASTEXITCODE
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
    # robocopy : 0 à 7 sont des codes de succès (information sur ce qui a été copié, etc.), 8 et plus signifient un échec
    throw "Échec de robocopy (ExitCode=$LASTEXITCODE)"
}

Depuis PowerShell 7.4, on peut modifier ce comportement avec $PSNativeCommandUseErrorActionPreference. Ajouté comme fonctionnalité expérimentale en 7.3, il est devenu une fonctionnalité officielle (mainstream) en 7.4.3 En le passant à $true, une commande native au code de sortie non nul déclenche une erreur non terminale précisant ce code, et cette erreur suit $ErrorActionPreference. Autrement dit, combiné avec Stop, l’échec d’une commande externe passe lui aussi par try/catch.12

# PowerShell 7.4 et versions ultérieures : gérer aussi l'échec des commandes externes avec try/catch (valeur par défaut : $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try {
    git.exe fetch origin
}
catch {
    Write-Warning "Échec de git : $($_.Exception.Message)"
    throw
}

& {
    # Pour une commande comme robocopy, où non nul ne signifie pas échec, on désactive
    # temporairement ce comportement dans un bloc de script et on revient au jugement
    # classique via $LASTEXITCODE (rétabli automatiquement à la sortie du bloc)
    $PSNativeCommandUseErrorActionPreference = $false
    robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
    if ($LASTEXITCODE -ge 8) { throw "Échec de robocopy (ExitCode=$LASTEXITCODE)" }
}

Comme le montre l’exemple de robocopy dans la documentation officielle elle-même, certaines commandes utilisent un code de sortie non nul comme une information normale, donc activer ce comportement de manière globale exige de concevoir des zones d’exception.2 Dans les environnements limités à Windows PowerShell 5.1, cette fonctionnalité n’existe pas : il faut donc s’en tenir uniformément au jugement par $LASTEXITCODE. Les différences de comportement entre 5.1 et 7 sont un piège classique lors d’une migration ; consultez également « Différences entre Windows PowerShell 5.1 et PowerShell 7, et migration ».

5. Conception des exit codes — permettre au Planificateur de tâches et à la supervision de juger du succès

Une fois l’erreur capturée, l’étape suivante consiste à la signaler à l’extérieur. Le seul moyen dont disposent réellement le Planificateur de tâches ou un outil de supervision pour connaître le succès ou l’échec d’un script est le code de sortie du processus. Fixons précisément la spécification.

  • exit <nombre> permet de fixer explicitement le code de sortie du script. exit définit également une valeur dans $LASTEXITCODE.59
  • Lancé avec pwsh -File (powershell.exe -File), la valeur passée à exit devient telle quelle le code de sortie du processus. Sans instruction exit, une fin normale vaut 0, et une terminaison sur exception non gérée vaut 1.56
  • Lancer le script avec -Command ne préserve pas un code de sortie comme exit 10 défini à l’intérieur du script. Il est ramené à 0 ou 1 selon le succès de la dernière commande (si exit 10 est écrit directement dans la chaîne de commande, cette valeur est renvoyée telle quelle). Si l’exploitation repose sur la distinction entre différents codes de sortie du script, lancer avec -File est la pratique standard.6

En traduisant cette spécification en squelette, un modèle de script sans supervision ressemble à ceci.

# Invoke-NightlyExport.ps1 — squelette permettant au Planificateur de tâches de juger du succès
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'   # Pour une exécution sans supervision, faire de "s'arrêter et signaler" le comportement par défaut

# Conserver une trace complète de l'exécution (sortie standard et erreurs comprises) comme journal (-Append pour ajouter au fichier quotidien)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append

try {
    Export-DailyData      # Logique métier principale (appelle une fonction issue d'un module)
    exit 0                # Signale explicitement le succès
}
catch [System.Net.WebException] {
    Write-Warning "Erreur de communication : $($_.Exception.Message)"
    exit 10               # Famille d'erreurs temporaires — laisse la possibilité de configurer une nouvelle tentative côté tâche
}
catch {
    Write-Warning "Erreur inattendue : $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1                # Erreur permanente — pas de nouvelle tentative, un humain doit l'examiner
}
finally {
    Stop-Transcript       # Dans finally, la trace se ferme même en sortant via exit
}

Start-Transcript est une applet de commande qui enregistre intégralement en texte les entrées et sorties d’une session, permettant de reproduire « ce qui s’affichait à l’écran à ce moment-là » sans avoir à mettre en place d’echo ni de redirection.10 Il ne s’agit pas d’une alternative exclusive à vos propres fonctions de journalisation : il vaut la peine de l’utiliser conjointement, comme ultime filet de sécurité. La conception des journaux et la lutte contre leur croissance excessive sont traitées dans « PowerShell avancé — automatiser en toute sécurité l’investigation, l’archivage et la mise en rapport des journaux ».

L’astuce pour l’attribution des exit codes est de ne pas trop compliquer les choses. Une granularité du type 0 = succès, 1 = erreur permanente (à examiner par un humain), et la dizaine 10 = erreur temporaire (nouvelle tentative acceptable) suffit largement, et se branche directement sur le « résultat de la dernière exécution » du Planificateur de tâches ou sur le jugement de succès d’un outil de gestion de jobs. Pour la configuration côté tâche (nouvelle tentative en cas d’échec, méthode de vérification du résultat), voir « Les tâches du Planificateur de tâches qui ne s’exécutent pas ou se terminent en 0x1 — isoler la cause et concevoir une exploitation fiable ».

6. Conception des nouvelles tentatives — distinguer erreurs temporaires et erreurs métier

Passons enfin à la réexécution. La valeur d’une nouvelle tentative est d’absorber automatiquement les erreurs temporaires et de ne pas réveiller quelqu’un en pleine nuit — mais l’ajouter sans discernement crée un autre type d’incident : retenter indéfiniment un échec permanent, ou corrompre des données par un double traitement. Il y a trois principes.

  • Ne retenter que les erreurs temporaires. Limitez-vous aux échecs que le temps peut résoudre de lui-même : coupure réseau momentanée, verrouillage temporaire d’un fichier, attente du démarrage d’un service dépendant. Faites échouer immédiatement les entrées invalides, les droits insuffisants ou les erreurs de configuration, et transmettez-les à un humain via l’exit code et les journaux.
  • Concevoir une limite sur le nombre et l’intervalle. Fixez un nombre maximal de tentatives, et élargissez l’intervalle avec un backoff exponentiel (2 secondes, 4 secondes, 8 secondes…). Marteler à intervalle fixe un interlocuteur en difficulté ne fait qu’entraver son rétablissement.
  • Rendre le traitement idempotent (sûr à réexécuter). Qu’il s’agisse d’une nouvelle tentative ou d’une réexécution par le Planificateur de tâches, cela signifie « le même traitement s’exécute à nouveau ». Cela suppose une conception telle que publier la sortie via un fichier temporaire suivi d’un renommage, ou enregistrer les identifiants déjà traités pour rejeter les doubles imports.

Sous forme de modèle, cela converge vers la structure suivante.

function Invoke-WithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [scriptblock] $Operation,
        # Si on passe 0 ou moins, la boucle se terminerait normalement sans jamais s'exécuter : on impose donc une valeur d'au moins 1
        [ValidateRange(1, 100)]
        [int] $MaxAttempts = 4,
        # Une valeur négative provoquerait une autre erreur dans Start-Sleep lors d'une nouvelle tentative : on la rejette dès la liaison des paramètres
        [ValidateRange(0, 3600)]
        [int] $BaseDelaySeconds = 2,
        # N'énumérer que les types d'exception qui valent la peine d'être retentés (par défaut : réseau et E/S)
        [Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
    )
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            # Recevoir d'abord la sortie dans une variable, et ne la retourner qu'après succès.
            # Retourner directement & $Operation ferait fuir une sortie partielle vers l'appelant
            # si une exception survient en cours de route, si bien qu'une nouvelle tentative
            # réussie livrerait deux fois les mêmes données
            $output = & $Operation
            return $output
        }
        catch {
            $ex = $_.Exception
            $isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
            if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
                throw   # Erreur métier, ou limite de tentatives atteinte — laisser échouer tel quel
            }
            # Plafonner le temps d'attente qui croît exponentiellement (pour ne pas trop
            # attendre même avec un grand nombre de tentatives, et ne pas dépasser la
            # plage acceptée par Start-Sleep)
            $delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
            Write-Warning "Échec (tentative ${attempt}) : $($ex.Message) — nouvelle tentative dans ${delay} secondes"
            Start-Sleep -Seconds $delay
        }
    }
}

# Utilisation : veillez à transformer l'opération cible en erreur terminale avec -ErrorAction Stop
Invoke-WithRetry -Operation {
    Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}

# Remarque pour retenter Invoke-RestMethod / Invoke-WebRequest sous PowerShell 7 :
# sous 7, un échec de communication arrive sous un type de la famille HttpRequestException
# plutôt que WebException comme à l'époque de la 5.1, donc il n'est pas retenté avec les
# réglages par défaut ci-dessus. De plus, une réponse HTTP d'erreur permanente comme 404
# arrive sous le même type ; dès qu'une réponse revient, il faut donc trier soi-même,
# via le code de statut, ce qui "vaut la peine d'être retenté"
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
    # -SkipHttpErrorCheck permet de recevoir même une réponse d'erreur sans exception, afin d'inspecter le code et de décider comment la relancer
    $r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
    if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
        # Ne lancer en HttpRequestException que les codes temporaires → sera retenté
        throw [System.Net.Http.HttpRequestException]::new("Erreur HTTP temporaire : $($r.StatusCode)")
    }
    if ($r.StatusCode -ge 400) {
        throw "Erreur HTTP permanente : $($r.StatusCode)"   # type différent, donc pas de nouvelle tentative
    }
    $r.Content | ConvertFrom-Json
}

Le point clé est que le choix des exceptions à retenter se fait explicitement, par type. Écrire « tout ce qui est capturé est retenté » conduit à attendre inutilement quatre tentatives même pour une erreur permanente comme un mauvais paramètre. Une fois en production, une manière réaliste de faire évoluer ce mécanisme consiste à ajouter à $RetryableExceptions les types d’erreurs temporaires réellement observés dans les journaux. Il est utile d’extraire une telle fonction commune dans un module pour la réutiliser (voir « Conception des paramètres et modularisation en PowerShell »). Par ailleurs, la logique de nouvelle tentative et de branchement des erreurs est justement le genre d’endroit où il vaut la peine d’écrire des tests Pester (« Tester PowerShell avec Pester — une approche pratique pour rendre les scripts d’exploitation plus difficiles à casser »).

7. Bonnes pratiques (tableau de décision)

Question Options Critère de décision
Comportement par défaut des erreurs Rester sur Continue / $ErrorActionPreference = ‘Stop’ en tête de script Une exécution sans supervision est plus sûre avec “s’arrêter et signaler”. Un script d’investigation interactif peut rester sur Continue2
Où l’on souhaite capturer Espérer que ça marche / Ajouter explicitement -ErrorAction Stop Les applets de commande génèrent surtout des erreurs non terminales. Ajoutez Stop explicitement à toute ligne que vous voulez capturer1
Succès d’une commande native Ignorer / Juger par $LASTEXITCODE / $PSNativeCommandUseErrorActionPreference de la 7.4 Environnement mixte avec la 5.1 : uniformiser sur $LASTEXITCODE. Si tout est en 7.4+, utiliser la nouvelle fonctionnalité avec des zones d’exception pour des commandes comme robocopy32
Signalement externe du succès Journaux seuls / Concevoir les exit codes et lancer avec -File Les journaux sont pour les humains, les exit codes pour les machines — les deux sont nécessaires. Lancer avec -Command écrase le code de sortie6
Trace d’exécution Journalisation maison seule / Utiliser aussi Start-Transcript Un filet de sécurité qui conserve aussi les sorties que votre journalisation maison ne capte pas (sortie standard d’une commande externe, etc.)10
Nouvelles tentatives Retenter toutes les erreurs / Limiter aux erreurs temporaires + backoff exponentiel + idempotence Retenter une erreur métier est un accident en puissance. Combinez plafond, intervalle et idempotence

8. Résumé

  • Les erreurs PowerShell se répartissent en erreurs non terminales et erreurs terminales ; les erreurs non terminales n’entrent pas dans try/catch par défaut. La pratique standard consiste à ajouter explicitement -ErrorAction Stop à toute commande que l’on souhaite capturer.
  • $ErrorActionPreference vaut Continue par défaut. Placez-le sur Stop en tête d’un script sans supervision pour prévenir structurellement l’incident consistant à avaler un échec et se terminer comme si tout allait bien.
  • L’échec d’une commande native n’entre pas dans catch par défaut. Jugez-le avec $LASTEXITCODE, ou, à partir de PowerShell 7.4, exploitez $PSNativeCommandUseErrorActionPreference.
  • Dans catch, consignez dans les journaux le type de l’exception, son message et sa position à partir de $_ (l’ErrorRecord), et placez le nettoyage dans finally. finally s’exécute même avec Ctrl+C ou un exit.
  • Signalez le succès ou l’échec à l’extérieur via l’exit code. Lancé avec -File, la valeur de exit devient telle quelle le code de sortie, ce qui permet au Planificateur de tâches ou à la supervision de distinguer succès et échec.
  • Les nouvelles tentatives suivent trois principes : limitées aux erreurs temporaires, backoff exponentiel plafonné, et idempotence. Faites échouer immédiatement les erreurs permanentes et transmettez-les à un humain.

Articles connexes

Domaines de conseil associés

Komura Software LLC prend en charge la revue de la conception de la gestion des erreurs et des nouvelles tentatives pour les traitements de nuit et les scripts récurrents, l’investigation des pannes intermittentes telles que « traité comme un succès alors que ça a échoué » ou « le script tombe une fois par mois », ainsi que l’amélioration de la qualité opérationnelle des scripts existants.

Références

  1. Microsoft Learn, about_Error_Handling. Sur les trois catégories d’erreurs (non terminale, de fin d’instruction, de fin de script), le fait qu’une erreur non terminale n’entre pas dans catch/trap par défaut, le mécanisme de promotion via -ErrorAction Stop (ActionPreferenceStopException et $_.Exception.ErrorRecord), le fait qu’un catch typé effectue la correspondance sur le type d’exception d’origine, les spécifications de $? et $LASTEXITCODE, le fait qu’un code de sortie non nul d’une commande native ne génère pas d’ErrorRecord par défaut, et le comportement de $PSNativeCommandUseErrorActionPreference.  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  2. Microsoft Learn, about_Preference_Variables. Sur la valeur par défaut Continue de $ErrorActionPreference, le fait que le paramètre -ErrorAction prévaut pour une commande individuelle, l’application du réglage à sa portée et à ses portées enfants, la valeur par défaut $false de $PSNativeCommandUseErrorActionPreference, et l’exemple de désactivation temporaire dans un bloc de script pour une commande comme robocopy qui utilise un code de sortie non nul comme information.  2 3 4 5 6 7

  3. Microsoft Learn, What’s New in PowerShell 7.4. Sur le passage de la fonctionnalité expérimentale PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) au statut de fonctionnalité officielle (mainstream) dans PowerShell 7.4.  2 3

  4. Microsoft Learn, Everything you wanted to know about exceptions. Sur l’accès aux informations d’exception via $_ dans un bloc catch, le fait qu’une commande munie de -ErrorAction Stop et une erreur Write-Error deviennent toutes deux gérables dans catch, et le motif de libération de ressources via try/finally.  2

  5. Microsoft Learn, about_Language_Keywords. Sur le fait que le mot-clé exit fixe le code de sortie et le reflète aussi dans $LASTEXITCODE, qu’un script lancé avec pwsh -File renvoie l’argument numérique d’exit comme code de sortie, et qu’en l’absence d’instruction exit, une fin normale vaut 0 et une exception non gérée vaut 1.  2 3

  6. Microsoft Learn, about_Pwsh. Sur la manière dont le code de sortie est déterminé lors d’un lancement avec -File, et sur le fait qu’un lancement avec -Command convertit en 1 tout code de sortie différent de 0 ou 1, ce qui nécessite exit $LASTEXITCODE pour le préserver.  2 3 4

  7. Microsoft Learn, about_Try_Catch_Finally. Sur la syntaxe de try/catch/finally, les blocs catch typés et les catch multiples, et le fait que le bloc finally s’exécute en cas de succès, d’erreur, d’arrêt par Ctrl+C, ou même lors d’un exit à l’intérieur de catch. 

  8. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sur le changement apporté par PowerShell 7, où $? ne devient plus $false simplement parce qu’une commande native a écrit sur stderr, mais seulement en cas de code de sortie non nul. 

  9. Microsoft Learn, about_Automatic_Variables. Sur le fait que $LASTEXITCODE conserve le code de sortie d’un programme natif ou d’un script, et qu’il est défini à 1 en cas de sortie sur exception, à la valeur du mot-clé exit, ou à 0 en cas de fin normale lors d’un appel via pwsh -File. 

  10. Microsoft Learn, Start-Transcript. Sur l’enregistrement des commandes et de la sortie console d’une session dans un fichier texte, l’ajout via -Append, l’emplacement et le nom de fichier par défaut, et l’arrêt via Stop-Transcript.  2

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Pourquoi PowerShell n'entre-t-il jamais dans le bloc catch alors que j'ai bien écrit un try/catch ?
Parce que la plupart des erreurs émises par les applets de commande (cmdlets) sont des erreurs non terminales (non-terminating error). Le try/catch ne capture que les erreurs terminales : une erreur non terminale affiche un message et laisse le traitement se poursuivre, sans jamais entrer dans le bloc catch. La solution standard consiste à ajouter -ErrorAction Stop à la commande que l'on souhaite capturer (ou à définir $ErrorActionPreference = 'Stop' en début de script). Cela promeut l'erreur non terminale en erreur terminale, que le try/catch peut alors traiter.
Comment choisir entre $? et $LASTEXITCODE ?
$? est une valeur booléenne indiquant si l'opération précédente a réussi ; elle est définie aussi bien pour les applets de commande que pour les commandes natives. $LASTEXITCODE est le code de sortie du dernier programme natif exécuté (ou du dernier script ayant appelé exit), et il ne change pas en cas d'erreur d'une applet de commande. Pour juger du succès d'une commande externe comme robocopy ou git, $LASTEXITCODE est le choix fiable, car il permet aussi de vérifier la signification précise du code de sortie. Gardez à l'esprit qu'un code de sortie non nul d'une commande native n'entre pas, par défaut, dans un bloc catch.
Comment faire en sorte que le Planificateur de tâches puisse juger du succès ou de l'échec d'un script PowerShell ?
Il faut définir explicitement un code de sortie avec le mot-clé exit à la fin du script (et dans chaque bloc catch), puis lancer la tâche avec pwsh -File (ou powershell.exe -File) pour pouvoir surveiller la valeur du « résultat de la dernière exécution ». Lancé avec -File, la valeur passée à exit devient telle quelle le code de sortie du processus ; sans instruction exit, une fin normale renvoie 0 et une exception non gérée renvoie 1. Lancer le script avec -Command a pour effet de convertir en 1 tout code de sortie différent de 0 ou 1 : si l'exploitation repose sur les codes de sortie, lancer le script avec -File est donc la pratique standard.
Pour quels types d'erreurs faut-il prévoir une nouvelle tentative ?
Il faut se limiter aux erreurs temporaires pour lesquelles un nouvel essai peut réellement changer le résultat (coupure réseau momentanée, verrouillage temporaire d'un fichier, attente du démarrage d'un service, etc.). Les erreurs métier ou permanentes, comme des données d'entrée invalides, des droits insuffisants ou une erreur de configuration, échoueront quel que soit le nombre de tentatives : il faut donc les laisser échouer immédiatement, sans nouvelle tentative, et en informer un humain via les journaux et l'exit code. Même lorsqu'on retente, il faut fixer une limite au nombre de tentatives et à l'intervalle, élargir cet intervalle avec un backoff exponentiel, et partir du principe que le traitement est conçu de façon idempotente pour qu'une réexécution ne provoque pas de double traitement.
Que fait le paramètre $PSNativeCommandUseErrorActionPreference introduit avec PowerShell 7.4 ?
C'est un paramètre qui déclenche une erreur PowerShell (une erreur non terminale) lorsqu'une commande native se termine avec un code de sortie non nul. Il a été ajouté comme fonctionnalité expérimentale dans PowerShell 7.3, puis est devenu une fonctionnalité officielle (mainstream) dans la 7.4 (la valeur par défaut est $false). En le passant à $true, l'échec d'une commande native suit $ErrorActionPreference ; combiné avec Stop, on peut donc capturer l'échec d'une commande externe avec try/catch. Cela dit, certaines commandes, comme robocopy, utilisent un code de sortie non nul comme information normale, il faut donc veiller à repasser temporairement ce paramètre à $false autour de ces appels.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog