Tratamento de Erros com C

por Fabio A. Mazzarino

Tratamento de erros em C pode se tornar um problema. É muito fácil se perder um conjuntos de ifs aninhados, aumentando cada vez mais a interpretação do código, dificultando, consequentemente, sua manutenção. Vamos ver um exemplo:

err = step1(); 
if (!err) {
    trace("Step 1 executado com sucesso", err);
    err = step2();
    if (!err) {
        trace("Step 2 executado com sucesso", err);
        err = step3();
        if (!err) {
            trace("Step 3 executado com sucesso", err);
        } else {
            trace("Falha na execucao de Step 3");
    } else {
        trace("Falha na execucao de Step 2");
    }
} else {
    trace("Falha na execucao de Step 1");
}
if (err)
    return false;
else
    return true;

Note a dificuldade de visualização do código crescente conforme a quantidade de passos no exemplo vai aumentando, os ifs vão se aninhando, dificultando o entendimento do código, e encarecendo sua manutenção.

Falhar Cedo

Uma boa técnica é falhar cedo, assim que uma falha for detectada finalizar o processamento. Por exemplo:

err = step1();
if (err) {
    trace("Falha na execucao de Step 1", err);
    return false;
}
trace("Step 1 executado com sucesso");

err = step2();
if (err) {
    trace("Falha na execucao de Step 2", err);
    return false;
}
trace("Step 2 executado com sucesso");

err = step3();
if (err) {
    trace("Falha na execucao de Steop 3", err);
    return false;
}
trace("Step 3 executado com sucesso");
return true;

Note como o fluxo de execução fica mais fácil de ser visualizado. O resultado dos dois trechos de código são equivalentes, mas o segundo exemplo é mais claro.

Lidando com Recursos

Falhar cedo funciona muito bem quando não estamos lidando com recursos que precisam ser liberados, por exemplo, com alocação de memória:

p1 = malloc(size1);
if (!p1) {
    trace("Falha na alocacao de memoria p1");
    return false;
}

p2 = malloc(size2); 
if (!p2) {
    trace("Falha na alocacao de memoria p2");
    free(p1);
    return false;
}

p3 = malloc(size3);
if (!p3) {
    trace("Falha na alocacao de memoria p3");
    free(p2);
    free(p1);
    return false;
}
processar(p1, p2, p3);
free(p3);
free(p2);
free(p1);
return true;

O exemplo utiliza malloc, mas serve qualquer tipo de função que consuma recursos que precisam ser liberados manualmente.

Percebe a dificuldade na manutenção do código? Para cada nova alocação é necessário gerenciar a liberação de memória em cada bloco subsequente, e no final da função também.

Sim, Temos Controvérsias

Uma técnica muito controvérsia é a utilização de goto. Por que? Porque muitos programadores consideram que o goto quebra a estruturação do código, rompendo o fluxo contínuo do programa.

Na verdade goto não é o único que faz isso, continue, break e throw também o fazem, e nem por isso são tão controversos.

Mas vamos demonstrar a solução primeiro:

success = false;
p1 = malloc(size1);
if (!p1) {
    trace("Falha na alocacao de memoria p1");
    goto err1;
}
p2 = malloc(size2);
if (!p2){
    trace("Falha na alocacao de memoria p2");
    goto err2;
}
p3 = malloc(size3);
if (!p3) {
    trace("Falha na alocacao de memoria p3");
    goto err3;
}
processar(p1, p2, p3);
success = true;

err3:
free(p3);
err2: 
free(p2);
err1: 
free(p1);
return sucess

Note como o gerenciamento de memória ficou mais simples, em um único lugar, a chance de ocorrer um vazamento de recursos é mais reduzida, e o custo de manutenção também.

Um problema dessa construção é que muitas equipes de desenvolvimento não a aceitam, por não aceitarem o goto como um comando de programação estruturada.. Mas decisões de metodologias de desenvolvimento são de responsabilidade de cada equipe, portanto, quando permitido, tratamento de erro com goto é uma boa opção.

Em um post futuro vamos tratar a sintaxe e as restrições do goto no C. E o porquê dele não oferecer risco.