Partager via


Instructions de gestion des exceptions : throw, try-catch, try-finally et try-catch-finally

Vous utilisez les instructions throw et try pour travailler avec des exceptions. Utilisez l’instruction throw pour lever une exception. Utilisez l’instruction try pour intercepter et gérer les exceptions qui peuvent se produire pendant l’exécution d’un bloc de code.

Instruction throw

L’instruction throw lève une exception :

if (shapeAmount <= 0)
{
    throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}

Dans une instruction throw e;, le résultat de l’expression e doit être implicitement convertible en System.Exception.

Vous pouvez utiliser les classes d’exception intégrées, par exemple ArgumentOutOfRangeException ou InvalidOperationException. .NET fournit également les méthodes d’assistance suivantes pour lever des exceptions dans certaines conditions : ArgumentNullException.ThrowIfNull et ArgumentException.ThrowIfNullOrEmpty. Vous pouvez également définir vos propres classes d’exception qui dérivent de System.Exception. Pour plus d’informations, consultez Création et levée d’exceptions.

À l’intérieur d’un bloc catch, vous pouvez utiliser une instruction throw; pour lever à nouveau l’exception gérée par le bloc catch :

try
{
    ProcessShapes(shapeAmount);
}
catch (Exception e)
{
    LogError(e, "Shape processing failed.");
    throw;
}

Notes

throw; conserve la trace de pile d’origine de l’exception, qui est stockée dans la propriété Exception.StackTrace. À l’inverse, throw e; met à jour la propriété StackTrace de e.

Quand une exception est levée, le Common Language Runtime (CLR) recherche le bloc catch qui gère cette exception. Si la méthode exécutée ne contient pas un tel bloc catch, le CLR examine la méthode qui a appelé la méthode actuelle, puis remonte la pile des appels. Si aucun bloc catch n’est trouvé, le CLR met fin au thread en cours d’exécution. Pour plus d’informations, consultez la section Comment les exceptions sont gérées de la spécification du langage C#.

Expression throw

Vous pouvez également utiliser throw comme expression. Cela peut être pratique dans un certain nombre de cas, notamment :

  • l’opérateur conditionnel : L’exemple suivant utilise une expression throw pour lever un ArgumentException lorsque le tableau args passé est vide :

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • l’opérateur de fusion de Null : L’exemple suivant utilise une expression throw pour lever un ArgumentNullException lorsque la chaîne à affecter à une propriété est null :

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • un lambda ou une méthode expression-bodied : L’exemple suivant utilise une expression throw pour lever un InvalidCastException pour indiquer qu’une conversion en valeur DateTime n’est pas prise en charge :

    DateTime ToDateTime(IFormatProvider provider) =>
             throw new InvalidCastException("Conversion to a DateTime is not supported.");
    

Instruction try

Vous pouvez utiliser l’instruction try sous l’une des formes suivantes : try-catch - pour gérer les exceptions qui peuvent se produire pendant l’exécution du code à l’intérieur d’un bloc try, try-finally - pour spécifier le code qui est exécuté lorsque le contrôle quitte le bloc try et try-catch-finally - en combinaison des deux formulaires précédents.

Instruction try-catch

Utilisez l’instruction try-catch pour intercepter et gérer les exceptions qui peuvent se produire pendant l’exécution d’un bloc de code. Placez le code là où une exception peut se produire à l’intérieur d’un bloc try. Utilisez une clause catch pour spécifier le type de base des exceptions que vous souhaitez gérer dans le bloc catch correspondant :

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

Vous pouvez fournir plusieurs clauses catch :

try
{
    var result = await ProcessAsync(-3, 4, cancellationToken);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Processing is cancelled.");
}

Lorsqu’une exception se produit, les clauses catch sont examinées dans l’ordre spécifié, de haut en bas. Au maximum, un seul bloc catch est exécuté pour toute exception levée. Comme le montre également l’exemple précédent, vous pouvez omettre la déclaration d’une variable d’exception et spécifier uniquement le type d’exception dans une clause catch. Une clause catch sans type d’exception spécifié correspond à n’importe quelle exception et, le cas échéant, doit être la dernière clause catch.

Si vous souhaitez lever à nouveau une exception interceptée, utilisez l’instruction throw, comme dans l’exemple suivant :

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{
    LogError(e, "Processing failed.");
    throw;
}

Notes

throw; conserve la trace de pile d’origine de l’exception, qui est stockée dans la propriété Exception.StackTrace. À l’inverse, throw e; met à jour la propriété StackTrace de e.

Un filtre d’exception when

En plus d’un type d’exception, vous pouvez également spécifier un filtre d’exception qui examine plus en détail une exception et détermine si le bloc catch correspondant gère cette exception. Un filtre d’exception est une expression booléenne qui suit le mot clé when, comme dans l’exemple suivant :

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

L’exemple précédent utilise un filtre d’exception pour fournir un seul bloc catch pour gérer les exceptions de deux types spécifiés.

Vous pouvez fournir plusieurs clauses catch pour le même type d’exception si elles se distinguent par des filtres d’exception. L’une de ces clauses peut ne pas avoir de filtre d’exception. Si une telle clause existe, il doit s’agir de la dernière des clauses qui spécifient ce type d’exception.

Si une clause catch a un filtre d’exception, elle peut spécifier le type d’exception qui est le même ou moins dérivé qu’un type d’exception d’une clause catch qui apparaît après elle. Par exemple, si un filtre d’exception est présent, une clause catch (Exception e) n’a pas besoin d’être la dernière clause.

Filtres d’exceptions par rapport à la gestion traditionnelle des exceptions

Les filtres d’exceptions offrent des avantages significatifs par rapport aux approches traditionnelles de gestion des exceptions. La différence clé est lorsque la logique de gestion des exceptions est évaluée :

  • Filtres d’exceptions (when) : l’expression de filtre est évaluée avant que la pile ne soit levée. Cela signifie que la pile d’appels d’origine et toutes les variables locales restent intactes pendant l’évaluation du filtre.
  • Blocs traditionnels catch: le bloc catch s’exécute une fois la pile déwound, ce qui risque de perdre des informations de débogage précieuses.

Voici une comparaison montrant la différence :

public static void DemonstrateStackUnwindingDifference()
{
    var localVariable = "Important debugging info";
    
    try
    {
        ProcessWithExceptionFilter(localVariable);
    }
    catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
    {
        // Exception filter: Stack not unwound yet.
        // localVariable is still accessible in debugger.
        // Call stack shows original throwing location.
        Console.WriteLine($"Caught with filter: {ex.Message}");
        Console.WriteLine($"Local variable accessible: {localVariable}");
    }
    
    try
    {
        ProcessWithTraditionalCatch(localVariable);
    }
    catch (InvalidOperationException ex)
    {
        // Traditional catch: Stack already unwound.
        // Some debugging information may be lost.
        if (ex.Message.Contains("traditional"))
        {
            Console.WriteLine($"Caught with if: {ex.Message}");
            Console.WriteLine($"Local variable accessible: {localVariable}");
        }
        else
        {
            throw; // Re-throws and further modifies stack trace.
        }
    }
}

private static void ProcessWithExceptionFilter(string context)
{
    throw new InvalidOperationException($"Exception for filter demo: {context}");
}

private static void ProcessWithTraditionalCatch(string context)
{
    throw new InvalidOperationException($"Exception for traditional demo: {context}");
}

Avantages des filtres d’exceptions

  • Meilleure expérience de débogage : étant donné que la pile n’est pas déwound tant qu’un filtre ne correspond pas, les débogueurs peuvent afficher le point de défaillance d’origine avec toutes les variables locales intactes.
  • Avantages en matière de performances : si aucun filtre ne correspond, l’exception continue de se propager sans la surcharge liée au déroulement et à la restauration de la pile.
  • Code plus propre : plusieurs filtres peuvent gérer différentes conditions du même type d’exception sans nécessiter d’instructions if-else imbriquées.
  • Journalisation et diagnostics : vous pouvez examiner et journaliser les détails de l’exception avant de décider s’il faut gérer l’exception :
public static void DemonstrateDebuggingAdvantage()
{
    var contextData = new Dictionary<string, object>
    {
        ["RequestId"] = Guid.NewGuid(),
        ["UserId"] = "user123",
        ["Timestamp"] = DateTime.Now
    };

    try
    {
        // Simulate a deep call stack.
        Level1Method(contextData);
    }
    catch (Exception ex) when (LogAndFilter(ex, contextData))
    {
        // This catch block may never execute if LogAndFilter returns false.
        // But LogAndFilter can examine the exception while the stack is intact.
        Console.WriteLine("Exception handled after logging");
    }
}

private static void Level1Method(Dictionary<string, object> context)
{
    Level2Method(context);
}

private static void Level2Method(Dictionary<string, object> context)
{
    Level3Method(context);
}

private static void Level3Method(Dictionary<string, object> context)
{
    throw new InvalidOperationException("Error in deep call stack");
}

private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
{
    // This method runs before stack unwinding.
    // Full call stack and local variables are still available.
    Console.WriteLine($"Exception occurred: {ex.Message}");
    Console.WriteLine($"Request ID: {context["RequestId"]}");
    Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
    
    // Return true to handle the exception, false to continue search.
    return ex.Message.Contains("deep call stack");
}

Quand utiliser des filtres d’exception

Utilisez des filtres d’exceptions lorsque vous devez :

  • Gérez les exceptions en fonction de conditions ou de propriétés spécifiques.
  • Conservez la pile d’appels d’origine pour le débogage.
  • Journaliser ou examiner les exceptions avant de décider s’il faut les gérer.
  • Gérez le même type d’exception différemment en fonction du contexte.
public static void HandleFileOperations(string filePath)
{
    try
    {
        // Simulate file operation that might fail.
        ProcessFile(filePath);
    }
    catch (IOException ex) when (ex.Message.Contains("access denied"))
    {
        Console.WriteLine("File access denied. Check permissions.");
    }
    catch (IOException ex) when (ex.Message.Contains("not found"))
    {
        Console.WriteLine("File not found. Verify the path.");
    }
    catch (IOException ex) when (IsNetworkPath(filePath))
    {
        Console.WriteLine($"Network file operation failed: {ex.Message}");
    }
    catch (IOException)
    {
        Console.WriteLine("Other I/O error occurred.");
    }
}

private static void ProcessFile(string filePath)
{
    // Simulate different types of file exceptions.
    if (filePath.Contains("denied"))
        throw new IOException("File access denied");
    if (filePath.Contains("missing"))
        throw new IOException("File not found");
    if (IsNetworkPath(filePath))
        throw new IOException("Network timeout occurred");
}

private static bool IsNetworkPath(string path)
{
    return path.StartsWith(@"\\") || path.StartsWith("http");
}

Conservation des traces de pile

Les filtres d’exceptions conservent la propriété d’origine ex.StackTrace . Si une catch clause ne peut pas traiter l’exception et lève de nouveau, les informations de pile d’origine sont perdues. Le when filtre ne démbobine pas la pile. Par conséquent, si un when filtre est false, la trace de pile d’origine n’est pas modifiée.

L’approche du filtre d’exception est précieuse dans les applications où la préservation des informations de débogage est essentielle pour diagnostiquer les problèmes.

Exceptions dans les méthodes asynchrones et itérateurs

Si une exception se produit dans une fonction asynchrone, elle se propage à l’appelant de la fonction lorsque vous attendez le résultat de la fonction, comme le montre l’exemple suivant :

public static async Task Run()
{
    try
    {
        Task<int> processing = ProcessAsync(-1);
        Console.WriteLine("Launched processing.");

        int result = await processing;
        Console.WriteLine($"Result: {result}.");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // Output:
    // Launched processing.
    // Processing failed: Input must be non-negative. (Parameter 'input')
}

private static async Task<int> ProcessAsync(int input)
{
    if (input < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
    }

    await Task.Delay(500);
    return input;
}

Si une exception se produit dans une méthode d’itérateur, elle se propage à l’appelant uniquement lorsque l’itérateur passe à l’élément suivant.

Instruction try-finally

Dans une instruction try-finally, le bloc finally est exécuté lorsque le contrôle quitte le bloc try. Le contrôle peut quitter le bloc try à la suite de

  • une exécution normale,
  • l’exécution d’une instruction de saut (c’est-à-dire, return, break, continue ou goto), ou
  • la propagation d’une exception hors du bloc try.

L’exemple suivant utilise le bloc finally pour réinitialiser l’état d’un objet avant que le contrôle quitte la méthode :

public async Task HandleRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    finally
    {
        Busy = false;
    }
}

Vous pouvez également utiliser le bloc finally pour nettoyer les ressources allouées utilisées dans le bloc try.

Notes

Lorsque le type d’une ressource implémente l’interface IDisposable ou IAsyncDisposable, considérez l’instruction using. L’instruction using garantit que les ressources acquises sont supprimées lorsque le contrôle quitte l’instruction using. Le compilateur transforme une instruction using en instruction try-finally.

L’exécution du bloc finally varie selon que le système d’exploitation choisit de déclencher une opération de déroulement d’exception. Les seuls cas où les blocs finally ne sont pas exécutés impliquent l’arrêt immédiat d’un programme. Par exemple, un tel arrêt peut se produire en raison de l’appel Environment.FailFast ou d’une exception OverflowException ou InvalidProgramException. La plupart des systèmes d’exploitation effectuent un nettoyage raisonnable des ressources dans le cadre de l’arrêt et du déchargement du processus.

Instruction try-catch-finally

Vous utilisez une instruction try-catch-finally pour gérer les exceptions qui peuvent se produire pendant l’exécution du bloc try et pour spécifier le code qui doit être exécuté lorsque le contrôle quitte l’instruction try :

public async Task ProcessRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    catch (Exception e) when (e is not OperationCanceledException)
    {
        LogError(e, $"Failed to process request for item ID {itemId}.");
        throw;
    }
    finally
    {
        Busy = false;
    }

}

Lorsqu’une exception est gérée par un bloc catch, le bloc finally est exécuté après l’exécution de ce bloc catch (même si une autre exception se produit pendant l’exécution du bloc catch). Pour plus d’informations sur les blocs catch et finally, consultez respectivement les sections L’instruction try-catch et L’instructiontry-finally.

spécification du langage C#

Pour plus d’informations, consultez les sections suivantes de la spécification du langage C# :

Voir aussi