Quais são as melhores práticas para capturar e repetir exceções?

156

As exceções capturadas devem ser lançadas novamente diretamente ou devem ser agrupadas em torno de uma nova exceção?

Ou seja, devo fazer isso:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

ou isto:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Se sua resposta for jogar diretamente , sugira o uso do encadeamento de exceções , não consigo entender um cenário do mundo real em que usamos o encadeamento de exceções.

Rahul Prasad
fonte

Respostas:

287

Você não deve capturar a exceção, a menos que pretenda fazer algo significativo .

"Algo significativo" pode ser um dos seguintes:

Manipulando a exceção

A ação significativa mais óbvia é lidar com a exceção, por exemplo, exibindo uma mensagem de erro e interrompendo a operação:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Log ou limpeza parcial

Às vezes, você não sabe como lidar adequadamente com uma exceção dentro de um contexto específico; talvez você não tenha informações sobre o "quadro geral", mas deseja registrar a falha o mais próximo possível do possível. Nesse caso, convém capturar, registrar e lançar novamente:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Um cenário relacionado é onde você está no lugar certo para executar uma limpeza na operação com falha, mas não para decidir como a falha deve ser tratada no nível superior. Nas versões anteriores do PHP, isso seria implementado como

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

O PHP 5.5 introduziu a finallypalavra - chave, portanto, para cenários de limpeza, agora existe outra maneira de abordar isso. Se o código de limpeza precisar executar, não importa o que aconteceu (por exemplo, com erro e com êxito), agora é possível fazer isso, permitindo com transparência a propagação de qualquer exceção lançada:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Abstração de erro (com encadeamento de exceção)

Um terceiro caso é onde você deseja agrupar logicamente muitas possíveis falhas sob um guarda-chuva maior. Um exemplo para agrupamento lógico:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

Nesse caso, você não deseja que os usuários Componentsaibam que ela é implementada usando uma conexão com o banco de dados (talvez você queira manter suas opções abertas e usar o armazenamento baseado em arquivo no futuro). Portanto, sua especificação Componentdiria que "no caso de uma falha de inicialização, ComponentInitExceptionserá lançada". Isso permite que os consumidores Componentcapturem exceções do tipo esperado , além de permitir que o código de depuração acesse todos os detalhes (dependentes da implementação) .

Fornecendo um contexto mais rico (com encadeamento de exceção)

Finalmente, há casos em que você pode fornecer mais contexto para a exceção. Nesse caso, faz sentido agrupar a exceção em outra que contém mais informações sobre o que você estava tentando fazer quando ocorreu o erro. Por exemplo:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Este caso é semelhante ao acima (e o exemplo provavelmente não é o melhor possível), mas ilustra o ponto de fornecer mais contexto: se uma exceção for lançada, ele nos informa que a cópia do arquivo falhou. Mas por que falhou? Essas informações são fornecidas nas exceções agrupadas (das quais poderia haver mais de um nível, se o exemplo fosse muito mais complicado).

O valor de fazer isso é ilustrado se você pensar em um cenário em que, por exemplo, a criação de um UserProfileobjeto faz com que os arquivos sejam copiados porque o perfil do usuário é armazenado em arquivos e suporta a semântica de transações: você pode "desfazer" as alterações porque elas são executadas apenas em um ambiente. cópia do perfil até você confirmar.

Nesse caso, se você fez

try {
    $profile = UserProfile::getInstance();
}

e, como resultado, ocorreu um erro de exceção "O diretório de destino não pôde ser criado", você teria o direito de ficar confuso. O agrupamento dessa exceção "principal" em camadas de outras exceções que fornecem contexto tornará o erro muito mais fácil de lidar ("Falha na criação da cópia do perfil" -> "Falha na operação de cópia do arquivo" -> "O diretório de destino não pôde ser criado").

Jon
fonte
Concordo apenas com os últimos 2 razões: 1 / lidar com a exceção: você não deve fazê-lo a este nível, 2 / logging ou limpeza: uso finalmente e log a exceção acima de sua DataLayer
bourgarel remi
1
@remi: exceto que PHP não suporta a finallyconstrução (pelo menos ainda não) ... Então, isso é fora, o que significa que temos de recorrer a coisas sujas como este ...
ircmaxell
@remibourgarel: 1: Isso foi apenas um exemplo. Claro que você não deve fazê-lo neste nível, mas a resposta é longa o suficiente. 2: Como diz @ircmaxell, não há finallyno PHP.
5134 Jon
3
Finalmente, o PHP 5.5 agora implementa finalmente.
OCDev
12
Há uma razão pela qual acho que você perdeu sua lista aqui - talvez você não consiga dizer se pode lidar com uma exceção até capturá-la e ter a chance de inspecioná-la. Por exemplo, um wrapper para uma API de nível inferior que usa códigos de erro (e possui zilhões deles) pode ter uma única classe de exceção da qual gera uma instância para qualquer erro, com uma error_codepropriedade que pode ser verificada para obter o erro subjacente código. Se você conseguir lidar com alguns desses erros de maneira significativa, provavelmente desejará capturar, inspecionar e, se não conseguir lidar com o erro, repita o processo.
Mark Amery
37

Bem, é tudo sobre manter a abstração. Então, eu sugiro usar o encadeamento de exceção para jogar diretamente. Quanto ao motivo, deixe-me explicar o conceito de abstrações com vazamentos

Digamos que você esteja construindo um modelo. O modelo deve abstrair toda a persistência e validação de dados do restante do aplicativo. Então agora o que acontece quando você recebe um erro no banco de dados? Se você repetir a exibição DatabaseQueryException, estará vazando a abstração. Para entender o porquê, pense na abstração por um segundo. Você não se importa como o modelo armazena os dados, apenas o faz. Da mesma forma, você não se importa exatamente com o que deu errado nos sistemas subjacentes do modelo, apenas que você sabe que algo deu errado e aproximadamente o que deu errado.

Então, ao reler o DatabaseQueryException, você está perdendo a abstração e exigindo que o código de chamada entenda a semântica do que está acontecendo no modelo. Em vez disso, crie um genérico ModelStorageExceptione envolva o que está DatabaseQueryExceptiondentro dele. Dessa forma, seu código de chamada ainda pode tentar lidar com o erro semântica, mas não importa a tecnologia subjacente do Modelo, pois você está apenas expondo erros dessa camada de abstração. Ainda melhor, desde que você quebrou a exceção, se ela borbulhar totalmente e precisar ser registrada, você poderá rastrear a exceção raiz lançada (percorrer a cadeia) para ainda ter todas as informações de depuração necessárias!

Não basta capturar e repetir a mesma exceção, a menos que precise fazer algum pós-processamento. Mas um bloco como } catch (Exception $e) { throw $e; }é inútil. Mas você pode refazer as exceções para obter algum ganho significativo de abstração.

ircmaxell
fonte
2
Ótima resposta. Parece que muitas pessoas em torno do Stack Overflow (com base em respostas etc.) estão meio que usando-as incorretamente.
James
8

IMHO, capturando uma exceção para apenas relançá-la é inútil . Nesse caso, apenas não o pegue e deixe que os métodos chamados anteriormente o tratem (também conhecidos como métodos que são 'superiores' na pilha de chamadas) .

Se você repetir a reprodução, encadear a exceção capturada para a nova que você lançará é definitivamente uma boa prática, pois manterá as informações que a exceção capturada contém. No entanto, relançar é útil apenas se você adicionar alguma informação ou manipular algo à exceção capturada, seja algum contexto, valores, registro, liberação de recursos, o que for.

Uma maneira de adicionar algumas informações é estender a Exceptionclasse, para ter exceções, como NullParameterException, DatabaseException, etc. Mais ainda, esta permitir que o developper apenas para pegar algumas exceções que ele pode manipular. Por exemplo, é possível capturar apenas DatabaseExceptione tentar resolver o que causou o Exception, como se reconectar ao banco de dados.

Clement Herreman
fonte
2
Não é inútil, há momentos em que você precisa fazer algo em uma exceção, digamos na função que a lança e depois lança-a novamente para permitir que uma captura mais alta faça outra coisa. Em um dos projetos nos quais estou trabalhando, às vezes capturamos uma exceção em um método de ação, exibimos um aviso amigável para o usuário e o lançamos novamente para que um bloco catch try mais adiante no código possa pegá-lo novamente para registrar o erro em um registo.
MitMaro
1
Então, como eu disse, você adiciona algumas informações à exceção (exibindo um aviso, registrando-o). Você não apenas repete como no exemplo do OP.
Clement Herreman
2
Bem, você pode apenas repassá-lo se precisar fechar recursos, mas não possui informações adicionais a serem adicionadas. Concordo que não é a coisa mais limpa do mundo, mas não é horrível
ircmaxell
2
@ircmaxell Acordado, editada para refletir que é inútil se você não faz nada com exceção rethrowing-lo
Clement Herreman
1
O ponto importante é que você perde o arquivo e / ou as informações de linha de onde a exceção foi lançada originalmente, lançando-a novamente. Portanto, geralmente é melhor lançar um novo e transmitir o antigo, como no segundo exemplo da pergunta. Caso contrário, ele apenas apontará para o bloco catch, deixando você adivinhando qual foi o problema real.
DanMan
2

Você deve dar uma olhada nas práticas recomendadas de exceção no PHP 5.3

O tratamento de exceções no PHP não é um recurso novo em nenhum momento. No link a seguir, você verá dois novos recursos no PHP 5.3, baseados em exceções. O primeiro são exceções aninhadas e o segundo é um novo conjunto de tipos de exceção oferecidos pela extensão SPL (que agora é uma extensão principal do tempo de execução do PHP). Esses dois novos recursos foram incluídos no livro de melhores práticas e merecem ser examinados em detalhes.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3

HMagdy
fonte
1

Você costuma pensar dessa maneira.

Uma classe pode lançar muitos tipos de exceções que não corresponderão. Então você cria uma classe de exceção para essa classe ou tipo de classe e lança isso.

Portanto, o código que usa a classe precisa capturar apenas um tipo de exceção.

Ólafur Waage
fonte
1
Ei, você pode fornecer mais alguns detalhes ou um link onde eu possa ler mais sobre essa abordagem.
Rahul Prasad