Should exceptions be used for flow control?

Asked

Viewed 2,538 times

34

When I studied Java, I learned that exceptions should only be used in situations, well, "exceptional" (i.e. an error, an unforeseen condition, etc), never for normal flow control. Not only because it is less performatic (creating an exception involves mounting a stack trace, among other things) but for being less "elegant" and "correct" - the ideal being to test the (pre-)conditions before making an operation or calling a method. Or, to signal an error to the caller, using a error code or similar. Only in really unpredictable cases would it be appropriate to use a try...catch.

However, in learning Python I came across a very different philosophy: in this language there are many cases where the recommended way (and sometimes the only way) to treat an unwanted condition is to try to run it and, if something goes wrong, capture the exception. There is no mention of performance, and this approach is often justified to prevent running conditions (e.g.: instead of testing if a file exists before opening it, simply open it at once! After all, it could be erased between testing and use, and if the program is not prepared for it its behavior in that situation may become incorrect).

I understand that if a language/platform follows a certain paradigm, it is advisable to stick to it, even if it is for reasons of consistency. But assuming that not only new languages can be created, but existing ones can be improved, I ask: are there clear advantages of one of these paradigms over the other? Or perhaps specific situations in which it is justified to use one or the other standard, even if it contradicts the recommendations "official"? What parameters should I observe when deciding between one approach and another? (by way of example, there are many situations where the race condition argument does not apply - when the data involved in the operation are immutable, for example.)

Finally, is there any reasonable middle ground that combines the benefits of both without the onus falling on the programmer? (for example, you could test if the file exists and wear a try/catch then, but that would be redundant) Note: I ask that this last point, if addressed in the reply, comes accompanied by reliable sources, to avoid mere speculation.

P.S. Some references demonstrating that this "everyday" use of exceptions is in fact quite recommended in the Python community, as opposed to most others:

  • 6

    Testing site boundaries (and computing) as always @mgibsonbr hehehe. I don’t know how to answer your question other than to say that there is every kind of religion and every kind of toy in the world. Python and Java are very different toys, the Python people tend to seek clean solutions from the point of view of LOC and readability; the Java people are more concerned with architecture Nterprise and well-defined flows. The standard for error handling follows the ecosystem culture.

  • 2

    That said, I’ve seen Python projects packed with tests to avoid try / except and Java projects where nothing is tested and all exceptions are wrapped in RuntimeException... I think we’re going to end up falling into a real discussion about schools of exception flow :).

  • 3

    @Anthonyaccioly Hehe does not doubt, but this only helps to legitimize the question: after all, it is not obvious that wrap everything in RuntimeException is it bad practice? Already do tests to avoid exceptions, not so obvious... There is an ideal point, but I lack more objective parameters to identify it than simply mine feeling case by case. I myself feel strange, always recommending to Java programmers (with whom I worked a long time ago) to avoid the abuse of exceptions, whereas in day-to-day working with Python I find myself forced to use them in a very different way... : P

  • 1

    @mgibsonbr Is it not the case to add the tag Python? Or even Python AND Java? The problems that are solved with exception flow in these languages are solved in a very distinct and more expressive way (regardless of right or wrong the code simply gets cooler) in C# or Ruby, for example.

  • @Caffé Personally, I preferred a more language-independent response, not only to avoid a "Gorilla vs. Shark" but mainly because my interest is in something that I can apply in a broader context (hence the tag design-de-linguagem and not característica-linguagem). A comparison/explanation about how the exceptions fit into the philosophy of these languages is only of little use to me (unless the arguments presented are capable of being extrapolated). However, if the community feels that the way it is becomes too broad, I have no objection to them.

  • @mgibsonbr I do not know if the context of this question can be broader than that of a single language. My suggestion is precisely to reduce the scope so as to avoid falling into comparisons. If the Python style is to use exceptions (all of a sudden more for lack of points resources in languages than for style), this is not the case for other languages. Where in Python it is more practical to use exceptions, in other languages there are much more practical and expressive ready resources. There is also a difference in cost per exception between languages - it is not in all of them that an exception is expensive.

  • 3

    @mgibsonbr, also came to take (and to be honest, still take) this matter of wrapping RuntimeExceptions generic by anti-pattern obvious. I went to complain at the source (our, it’s been 4 years) and I took a reversed. As I said, schools of thought :).

Show 2 more comments

4 answers

20


Making exceptions is legal!

Cool approach 1 - avoid exceptions because they represent evil:

public Resultado baixaEstoque(Produto produto, int quantidade) {

    Resultado resultado = new Resultado();
    
    if (!produtoDisponivel(produto, quantidade)) {
        resultado.isSucesso(false);
        resultado.setMotivoFalha(
            String.Format("Não há %d items de %s disponíveis em estoque.", 
            quantidade, produto.getNome()));
        return resultado;
    }
    // ... (baixa o estoque de fato);
    resultado.isSucesso(true);
}

The consumption at the highest level of the application is thus:

Resultado Resultado = estoque.baixaEstoque(produto, quantidade);
if (Resultado.isSucesso()) {
    mensagens.Add("O produto foi baixado do estoque");
} else {
    mensagens.Add(Resultado.getMotivoFalha());
}

I like the above code. No exceptions thrown, no try-catch was used.

Now consider a slightly more complex operation like closing a sale:

public Resultado fechaVenda(Carrinho carrinho, Cliente cliente, FormaPagamento formaPagamento) {

    Resultado resultado = crediario.reservaCredito(cliente, 
        carrinho.valorTotal(), formaPagamento);
    
    if (!Resultado.isSucesso()) {
        return resultado;
    }
    
    for (CarrinhoItem item : carrinho.getItems()) {
        resultado = estoque.baixaEstoque(item.getProduto(), item.getQuantidade);
        
        if (!Resultado.isSucesso()) {
            return resultado;
        }
    }
    
    resultado = crediario.debitaCliente(cliente, carrinho.valorTotal(), formaPagamento);
    
    if (!Resultado.isSucesso()) {
        return resultado;
    }
    
    resultado = despacho.ordenaDespacho(cliente, carrinho.getItems());
    
    if (!Resultado.isSucesso()) {
        return resultado;
    }
    // ... (mais operações de baixa de venda);
    return resultado;
}

So, reporting success or failure as return of method left me with several ifs and enough repeated code that does not express the business rule but is there just in case something happens that does not allow the system to follow its standard path.

Cool Approach 2 - use exceptions rationally for a cleaner, more expressive design:

Now consider this same closing operation of a sale using exceptions, where each process casts business exception in cases of predicted exceptions (such as a product not being available in stock). In this approach, the stock drop, for example, is like this:

public void baixaEstoque(Produto produto, int quantidade) {

    if (!produtoDisponivel(produto, quantidade)) {
        throw new BusinessException(
            String.Format("Não há %d items de %s disponíveis em estoque.", 
            quantidade, produto.getNome()));
    }
    // ... (baixa o estoque de fato);
}

Each process follows this same pattern, so that the complete process of closing sales is thus:

public void fechaVenda(Carrinho carrinho, Cliente cliente, FormaPagamento formaPagamento) {

    crediario.reservaCredito(cliente, carrinho.valorTotal(), formaPagamento);
    
    for (CarrinhoItem item : carrinho.getItems()) {
        estoque.baixaEstoque(item.getProduto(), item.getQuantidade);
    }
    
    crediario.debitaCliente(cliente, carrinho.valorTotal(), formaPagamento);
    
    despacho.ordenaDespacho(cliente, carrinho.getItems());
}

And the consumption at the highest level of the application is like this:

try {
    vendas.fechaVenda(carrinho, cliente, formaPagamento);
    mensagens.Add("O produto foi baixado do estoque");
} catch (Exception e)
    mensagens.Add(e.getMessage());
}

Ideally the application has a generic exception treatment that will already log in and add the error message, so the code gets even cleaner:

vendas.fechaVenda(carrinho, cliente, formaPagamento);
mensagens.Add("Venda concluída.");

In this second approach I eliminated dozens of ifs (would be many more in a complete code) and at the same time did not include any try-catch no part of my business code, even releasing useful exceptions!

In time: although the exceptions are foreseen, they are still exceptions. For example: a sale process does not reach its closure if the product is not previously available in stock, eventually it was reserved when added in the virtual cart. The product is no longer available at the time of your discharge is an exception that may occur in the case of a sales process with overbooking or in the case of competing sales when two bookings of the same product have already expired and the purchase was still carried through. So, may happen, but it’s an exception. Also, this exception is not determining the program flow - on the contrary, it is interrupting the program flow.

Conclusion 1:

Throwing exceptions is not wrong. On the contrary, it can help make code more expressive.

A premise of these first two approaches is that there is a transaction control around the higher level call, and that this transaction control ensures the consistency of the system state (executes a rollback) in case of failure. The second approach, for example, could rely on automatic and implicit transaction management of an EJB container, where no additional code would be required. In the first approach it would be necessary to include an explicit call to the rollback.

Weird approaches:

There is also the exception treatment by himself, without any purpose or that trial add robustness to code. Example:

try
{ 
    // ... (código de negócio);
}
catch (Exception e)
{
    e.printStackTrace();
}
// ... (continua com mais código de negócio ignorando completamente que houve exceção!);

Or else:

try
{ 
    // ... (código de negócio);
}
catch (Exception e)
{
    e.printStackTrace();
    throw e;
}

And there’s also the exception treatment that controls the program flow in a weird way:

public Pessoa obtemPessoa(String cpf) {
    try
    { 
        // ... (busca pessoa);
    }
    catch (Exception e)
    {
        JOptionPane.showMessageDialog("CPF não cadastrado.");
        return null;
    }
}

In my second approach, in a way i used exceptions to control the program flow for I have made exceptions to stop this flow, and that is not wrong. It was an example of good use of language resources.

In this code above the exception is also being used to control the flow, but to give an alternative flow instead of simply interrupting it; and yet it does so using a very weird code that mixes presentation with business rules.

Conclusion 2:

In various languages, casting and capturing exceptions without understanding why and not being right in this why (many of the reasons are just myths or lack of knowledge) is the problem. Not the exceptions themselves.

Necessary approaches:

Sometimes we do not question an exception but this is the simplest feature or the only acceptable performance feature or even the only possible way that the language offers us.

Example in Java:

    int valor;
    try
    { 
        valor = Integer.parseInt(str);
        // ... (usa o valor);
    }
    catch (NumberFormatException e)
    {
        // ... (código alternativo para string não válida);
    }

When in C#, I prefer to do so:

var numeroValido = Int32.TryParse(str, out valor);
if (numeroValido) 
    // ... (usa o valor);
else
    // ... (código alternativo para string não válida);

The code in Java is not wrong because this is the Java way to do it. When there is a way as expressive as the C#, I will use it.

Conclusion 3:

Like it or not to cast and capture exception, if this is the way to solve a certain problem in that language, there is nothing to discuss - we will simply cast and capture the exception.

Responses to your specific points:

When I studied Java, I learned that exceptions should only be used in "exceptional" situations (i.e. an error, an unforeseen condition, etc.), never for normal flow control.

Incorrect. Exceptions in Java are there to be used. Neither of my first two approaches is incorrect in a given scenario. So instead of "ever use" I prefer "use in a judicious manner".

Releasing an exception is less performative (creating an exception involves mounting a stack trace, among other things)

Correct. In my approach using exceptions, only 1 exception will eventually be thrown at each process execution, and in a process that depends on user interaction this cost is not sensitive. If you have a large batch of calls to a function, such as an integration, it is not a good idea to cast millions of exceptions. It turns out that from a design point of view will also not be a good idea, because in a batch integration you do not need an exception but a log. In time: anyway I’ve seen integration failing and releasing millions of exceptions and yet the end of the world did not come.

Using exceptions like normal flow can be less "elegant" and "correct".

True. But I have used exceptions in my second approach and it is not inelegant, nor is it incorrect. So again, the problem is not with the exceptions, but with the use we can make of it - as with any other feature of a programming language. Even the most basic feature of most languages, the IF, is extremely misused, and you don’t see many people saying never use it.

The ideal being to test the preconditions before making an operation or calling a method. Or, to signal an error to the caller, use an error code or similar.

Are design decisions, which consider requirements even non-functional, it is not possible to generalize what is right or wrong. In my first approach I used error code, in the second I used exceptions. In my opinion, the second approach is better (cleaner and more expressive code). And in your?

Only in the really unpredictable cases would it be appropriate to use a Try....

On the contrary, is precisely when you predict an error that you should worry about capturing the error, whether at the highest level of the application to show a message or more localized, in the business code, to tread an alternative path.

If you don’t know which mistake to wait for, better no try-catch. Let the application pop or show a message at the highest level of the application user friendly and informative at the same time for support.

However, when learning Python I came across a very different philosophy: in this language there are many cases where the recommended way (and sometimes the only way) to treat an unwanted condition is to try to run it and, if something goes wrong, capture the exception.

If this is the Python way, for several factors that I don’t know, or sometimes it’s the only way, simply there is nothing to discuss - it will have to be done this way.

But assuming that not only new languages can be created, but existing ones can be improved, I ask: there are clear advantages of one of these paradigms over the other?

It’s language that will say. Go (or go lang) has no exception but has other sophisticated features that Java and C# does not yet have. If the discussion can be brought to a specific language, then yes we can describe the advantages of one paradigm over the other.

What parameters should I observe when deciding between one approach and another?

When you can choose the approach, the only parameter is the code expression. It turns out that any good design judgment is sometimes simply thrown away by another that sometimes has more: performance. Let this not serve as a license to throw all the good design in the trash because the system needs to be ultra performatic. There are standards, precisely of design, to ensure that the non-functional requirements of a critical process do not affect the design of the entire system.

Finally, is there some reasonable middle ground that combines the benefits of both without the onus falling on the programmer?

Generalizing this question a little more than its original purpose, the answer is nay. The burden of the decision will always stay with the programmer. There is no mathematical recommendation that fits all cases. I don’t have much doubt about casting or how to treat exceptions, however I have enormous difficulty in passing on this security. Every programmer needs to worry about doing things in an ever better way so as to develop an experience that leads to a good and simple design, using or not exceptions.

Final conclusion

Maybe the book The Pragmatic Programmer delivered gold in a simple and symmetrical way between all languages with exceptions:

According to the book (referenced by Martin Fowler):

We believe that exceptions should rarely be used as part of the normal flow of a program: exceptions should be reserved for unexpected events. Assume that an undetected exception will terminate your program and ask yourself, "Will this code continue to run if I remove all exception treatments?" If the answer is "no", then perhaps the exceptions are being used in non-exclusive circumstances.

Code example that meets this premise:

public synchronized void baixaEstoque(Produto produto, int quantidade) {
    if (!produtoDisponivel(produto, quantidade)) {
        throw new BusinessException("Produto não disponível nesta quantidade");
    }
    // ... (baixa o estoque de fato);
}

Consumer code at the highest application level:

if (estoque.produtoDisponivel(produto, quantidade)) {
    ReservaProduto reserva = estoque.reservaProduto(produto, quantidade);
} else {
    mensagens.add("Produto não disponível nesta quantidade");
}
// e, em outra requisição:
try { 
    estoque.baixaProduto(reserva.getProdutoReservado(), reserva.getQuantidadeReservada());
} catch (BusinessException e) {
    mensagens.add(e.getMessage());
}

The stock expects that what is not available will not be downloaded, but as it is its function to treat competition, it knows that there is the unfortunate possibility that the product will no longer be there when the effective low.

However, if I remove the exception treatment (remove both the throw as to the catch), on the happy path (the default and most likely path) the code will continue to work.

Code example that DOES NOT meet this premise:

public Cliente obtemCliente(String cpf) {
    Cliente cliente = db.find(Cliente.class, cpf);
    if (cliente == null) {
        throw new CpfNaoCadastradoException(cpf);
    }
    return cliente;
}

The e-commerce just went live. The first user filled a cart and when ordered to close the purchase your CPF was requested. And then the following code was invoked:

try {
    Cliente cliente = obtemCliente(cpf);
} catch (CpfNaoCadastradoException) {
    redirecionaParaCadastroCliente(cpf);
}
solicitaLogin(cliente);

In the above example, if I remove the exception handling the code will not run even in the first attempt to use the system.

  • Good considerations! But my interpretation of the final quote is opposite to yours. The unfortunate path is reasonably probable, and the application may not break by the unavailability of the product.

  • @bfavaretto The point is the code to run or not to run. The code runs without exceptions and depending on business requirements may never break (consider a broad minimum stock and low sales volume; or a long cart expiration time associated with the target audience profile that quickly completes your purchases). Thanks for your comment - I added an example of code that uses exceptions and will break immediately if they are removed.

  • I understood Caffé, it will depend on the situation. Thanks for the return.

  • I have a code similar to your sales lock stream only each step consists of sending a command to a remote device and processing the received response. Treating the loss of connection as an exceptional case makes the code much more expressive, but I hesitate to classify this situation as exceptional, because the connection is often lost; if the Pragmatic Programmer criterion were applied the program would eventually fall, that is, "maybe it’s a non-exception". But if I understand well the question is not whether falls or does not fall and yes if the fall is rule or exception (even if frequent), correct?

  • @Piovezan What determines if it is an exception is not the frequency with which it occurs, nor if it was expected or not, but if should or not to happen. It’s hard to speak for someone else’s project, but from what you said I think the system counts that an established connection is necessary; so I would make a code that expresses that the connection is there, and treat the loss of connection as an exception, just like you did.

13

Maturity of Design

Design many variables depend on code: experience, context, language support and probably others.

Particularly, in several situations I end up experiencing different ways to implement a certain functionality until I reach one that is sufficiently "clean and robust enough" for me.

However, ideas of different approaches and knowledge about the consequences of design only come in time, with the experience of seeing the code breaking and understanding the reasons for it to occur. (Good companies look for developers who put code into production and not just those who code and leave it for others to fix).

There is a certain convergence

As for the use of exceptions in different languages, I don’t think there’s actually such a glaring difference between what programmers consider good code, whether in Python, C# or Java. Perhaps a consensus can be reached more or less independent of the resources offered by the language.

First, exceptions are not only for errors in Java. There is, for example, the exception InterruptedException which can be fired at certain events of a thread. We can also consider frameworks assertions like Junit as a way to indicate the state of the system.

Flow Control

We need to be careful with absolute rules. In general, when we say that it is wrong to treat flow with exceptions we should actually define what types of flows, because many of them are inherently exceptional.

When we talk about use cases, we can usually subdivide a case into a main scenario, alternative scenarios and exceptional scenarios. An excerpt of code can be similarly divided. There is a main flow of code, there may be conditionals to determine alternatives and exceptional treatments that are part of what is expected by the system.

Conditional flows

Imagine a system that displays a field that may or may not have value:

try
    cliente.imprimeNome()
catch NomeVazioException e
    print "sem nome"

In the example, the function imprimeNome() would make an exception if the field was not filled in. This is bad because it makes no sense to throw an exception to check a condition, it is totally counterintuitive.

A parole would be much better:

print cliente.isNomeEmpty() ? "sem nome" : cliente.getNome() 

Problems of Design

Another factor that leads to misuse of try/catch is a design bad of the code.

Consider:

try
    tentativa1();
catch Erro1 e1
    try
        tentativa2();
    catch Erro2 e2
        try
            tentativa2();
        catch Erro2 e2
            print "não deu"

The above example is "ugly" in any context, not because it is an attempt to control the flow, but because it smells like design (code Smell).

Exceptional flows

There are situations where exceptions are good to control the flow, after all they are naturally exceptional flows.

Let’s imagine a case of validation:

try
    assertValid(cliente);
    cliente.save()
catch ValidationError e
    print e.errors()

The same example would work perfectly with conditional:

e = validate(cliente)
if e.hasErrors()
    cliente.save()
else 
    print e.errors()

However, whether Java or any other language, the first example is much more readable.

Moreover, it does not seem "natural" to return error objects in methods. If a given error scenario returns need to return values, then an exception must be carefully considered to contain these values.

Using good practice when you shouldn’t

The example of testing whether a file exists before accessing it perfectly illustrates the problem of trying to apply good practice when it should not be done.

Consider the code:

if exists(file)
    read(file)
else 
    print "cannot read file"

Although it is very readable, there are two main problems:

  1. The file may cease to exist on the second line
  2. The file not existing is an exceptional scenario, so we would be doing the opposite: treating exceptional flow with conditional.

Therefore, it is more appropriate to do:

try
    read(file)
catch SomeFileException e
    print "cannot read file"

The advantage of treating exceptions in this way also makes it possible to treat multiple conditions simultaneously. No matter if the file does not exist, if it is empty or if there is no read or write permission, all these streams (in some cases) can be treated specifically in a block catch. A parole here would be verbose and unnecessary.

Granularity of the exceptions

Personally, I don’t like very granular or specific exceptions. At least until I need them to be.

I’m also against methods that start with a try and end with a catch, capturing any exception.

The best way I know to code is to define groups of exceptions. If treatment is required for a specific type of exceptional case, then this case deserves an exception of its own.

A common example of this refers to invalid arguments. Example:

if param1 == null
    throw new IllegalArgumentException

Another common refers to field validation errors. We often do validations like this:

if cliente.nome == null
    throw new ValidationError("name cannot be null")

But in certain cases one of the exceptional flows deserves isolated treatment:

if findByCpf(cliente.cpf)
    throw new CpfJaCadastradoException

In this case, an already registered CPF could take some specific action, such as loading the client’s data through the CPF.

The vision of the problem

Depending on how the domain is modeled, we can have more of a correct and coherent implementation from both the point of view of business rules and coding.

Example 1:

try 
    transferencia(cliente1, cliente2, 10)
catch FundosInsuficientesException e
    print "sem saldo"

Example 2:

if temSaldo(cliente1, 10) 
    transferencia(cliente1, cliente2, 10)
else 
    print "sem saldo"

Whereas both examples contain transactional blocks and do not allow concurrent access to accounts, then we have two valid implementations.

The first considers the lack of balance as an exceptional scenario. This would make sense, for example, in features where the user would already have the balance validated on a previous screen. Imagine that the system only shows the transfer amounts that are smaller than the current balance. The possibility of the user trying to make a larger transfer than the balance is a rare case of the account being moved while it selects the amount to be transferred.

The second considers that the lack of balance is an alternative and expected scenario.

The difference is more semantic, related to the understanding of what is "alternative" or "exceptional". In short, it depends on how you see the problem.

12

Exceptions should not be used for flow control

There is no doubt that exceptions should not be used for flow control. There is a lot of literature on this. Even every Java programmer should know this because everyone read Joshua Bloch’s book, Effective Java which makes this very clear, even though the API does not always respect this aspect. In C++ many style guides prohibit the use of exceptions, even for technical issues specific to the language. In C# people more involved with the language and experienced programmers usually say the same, even if some books and less respectable articles say otherwise.

Exceptions are flow control

But let’s not fool ourselves. Exception is not a memory control and is not a calculation. It is a flow control. And one of the most difficult to track and predict what will happen in the application, especially when you need to debug it. So there is no such dichotomy as the question indicates. Although it is understandable that it exists. There are many guilty for this interpretation, including I help a little bit.

Let’s understand this inconsistency

I decided to answer because I I often talk about it. The problem is that the definition is purposefully vague and cannot be interpreted literally. It is vague because you can not define very well what is an effectively exceptional situation and what is a simple flow control that can be better manipulated without the exception mechanism. The secret is to define whether the situation is exceptional or not.

That’s probably where Python is less selective. It is possible to define that anything that does not produce exactly the desired result is an exceptional situation. And every time a result is not expected you have to at least consider that there will be a flow control.

The problem is that in many cases it is really expected that the result will be out of the ordinary. Is this exceptional or not? I liked the question because it made me reflect how this statement so decanted is ideal to pass the idea you want. You see, I’m not saying that the concept is wrong, on the contrary. The problem may lie with the simplistic phrase. I know what she means, but it’s easy for a person who still doesn’t fully understand its meaning to have a mistaken idea of what it means.

Performance is also important

There is a performance issue that should be considered as well. The exception is a slow mechanism, so far, in all languages. Each with a different commitment but all of them cost a lot of money when used. Most only have a higher cost when it is launched, although none has zero cost just to score when it can be captured. When the code being created will be used, even potentially, in something that requires maximum performance, it is clear that the exception cannot be used. Even if an API is required and uses an exception, the programmer will probably have to create a custom alternative. And let’s face it, anything that has just basic, generic processing has the potential to be used on something that requires performance.

When dealing with external resources, when the desired operation involves IO, including UI, even if indirectly where will use the exception, performance becomes less relevant.

Performance is Feature. For some languages this is not so true. In Python and other compiled dynamic and/or pseudo languages it has no philosophy of having the most performance, so one of the reasons, which is the performance, not to use exception is irrelevant.

Everyone already knows, or should know, that performance should not be sought until it is necessary. I imagine that Python assumes that if you need to avoid the cost of the exception then you are chipped with so many other things that the language imposes on your code that this will not cause major problems.

Java uses exceptions in many cases where performance may be important, so often, if it imports, the programmer will probably have to write a new API avoiding the use of the exception in it.

Expressive code

In the question and answer pair cited above in the original author’s question I talk about how there are other mechanisms to deal with results other than normal and how other flow control mechanisms can be used without problems. But both here in the question and there, it is shown that there are cases where the exception becomes fundamental, or at least makes the code more expressive when it uses it. I show in the file access example how it is simpler to control the exception than to keep checking each error to continue the normal flow. Because not only can one be needed if for each line, but may require the lines to be broken to handle all problems individually, which is probably not necessary.

So if using the exception makes the flow more effective when a different situation occurs, it is not a problem to use it.

It is also necessary to examine whether the efficiency will not be compromised at improper levels.

The problem happens to exist when the programmer is not using it consciously. Or when it creates an exception when it doesn’t know how it will be handled. Therefore some Java Apis are not the most suitable ones. Language has the philosophy of performing, though not in the extreme, and also that it should not be necessary to use other auxiliary languages when performance is critical - unlike Python who preaches the aid of secondary language in these cases, C. Java probably has Apis that don’t guarantee performance in things that require performance. In many Apis specialized in several languages this occurs as well. This is the error.

Python also greatly encourages things to be resolved in Runtime, what helps to adopt this philosophy. Languages like this, for better or for worse, think of flexibility, which brings unpredictability, which is something natural in exceptions. It is no use to say that it values predictability when it has unpredictable mechanisms in its essence, starting with dynamic typing. So it’s one more point that corroborates against philosophy.

In short, exceptions should be used when you want them to be captured and manipulated. When you are creating a mechanism where the exception is more suitable to deal with the flow than other simpler mechanisms. I repeat what I always say: do what you need, as long as you know what you’re doing, that you understand why you’re doing it, that you have a real gain without having a significant loss. Don’t learn rules, but understand the whole process.

I only have the hammer

And let’s not fool ourselves. Exception is a complicated mechanism. It is a goto in its worst form. What strikes me is the attitude of some programmers against the goto that tells you to go to the center of your city, but they don’t mind sending you to the center of some city that even you don’t know exists and you’ll have to figure out which center to stop at according to what the transport company takes you to. So yes, it should be avoided. Not at any cost, but consciously. It should be used whenever it is the best solution, just as in goto. Like the goto is simpler, and perhaps because it’s something older, there are more ways to replace it.

But more modern languages are creating other mechanisms to make semantics more expressive than is desired. Even today we use the exception when the situation could be manipulated differently just because it lacks a better tool.

I understand that the question has a collateral goal of seeking better solutions to control the flow in situations that are not truly exceptional. Even some languages have better solutions than exceptions to control flow when you need to have a consolidated action for several possible steps, as the example in the accepted answer shows. That is, we often use exceptions to control flow because the language gives no better alternative.

Nor will I enter into the discussion that programming errors are not exceptional situations in fact. Java was almost right to treat this a little differently. Not that it would change much but it would be interesting to have a separate mechanism to handle programming errors that can only be detected at runtime.

So again, we should not use exceptions for control of normal flow, until the language used does not offer a better tool. There we are obliged to hammer with screwdriver. When you’re creating a language, consider this. And also consider that having several mechanisms for every possible semantics in the code complicates the language. Choose your trade-off.

Completion

Only the programmer can define whether the situation is exceptional or not. The same situation can be in one application and not be exceptional in another. It will not be common for this to happen if there is consistency but it may be valid to have a different treatment. One of the problems with generic Apis is that they don’t consider it. So it can be very suitable to use in one code and inadequate in another. At this point it makes sense to have a Parse() and a TryParse(), although having this "duplication" is also a problem.

Learn why the mechanism exists and avoid the cult post.

Alternative mechanism

Finally, out of curiosity, I want to introduce a little-known mechanism that has been implemented in the language Clipper that I worked a lot of my career and still use indirectly with the Harbour. It has some mechanisms to deal with situations that are not the normal flow, including exceptions, although they are almost never used.

Clipper has created a centralized error handling mechanism that works very well for its purpose. Clipper never intended to solve all problems, but solve some well. Because of this, with some apparent limitations, it works well in applications of millions of lines and software written on it survives for decades. This mechanism uses a kind of Singleton error that knows how to treat each type of error. Language does not encourage to keep creating specialized types. So whenever an error occurs, this object is called to know what to do. Most likely what should be done with a type of error should be the same for every application, so it works very well. And when you need a more specific manipulation, it’s possible set one lambda with specific handling of this subsystem. Which, if you think about it, at least in this part, is similar to what the try-catch does, or how other mechanisms of other languages, such as scope of D or defer Go. So instead of a throw that makes the unwind from the stack, it calls a custom action. This is useful in the vast majority of situations where you need to do something close to the error, i.e., you just want to run something, you don’t want to disassemble the application. This system of errors allows to continue the execution, to retain or to abort and then will do the unwind, if necessary.

It is possible to use something similar in most languages, just by creating a library and adopting certain patterns. Of course, not all languages will make it work as well as possible.

I stress that this mechanism does not replace the exception but creates a way to deal with certain problems. The fact is that if the language has better mechanisms, they should be used and the exception should be left only when it really is the best solution.

5

It is a very relative question (language, culture, community, ...) and, based on my knowledge, my answer tends to be: it depends. There are cases and cases, for example, I can answer by Java:

I don’t need to check with a regular expression (or a bunch of ifs) if any string can be transformed into a date, I can just try to format it and capture the exception ParseException in case of problems. But, yes, I need to check if an element is null rather than generically capture NullPointerException´s by my code, after all maintaining an API contract helps in the maintenance of the code, in its testability and enhances its quality (avoiding errors). As you said yourself, dealing with pre and post conditions is one way to solve this problem and, using your example, you can rather test whether the file exists and still treat the exception, it is not redundant:

  • file does not exist; tests the pre-condition -> code is not executed and exception is not generated
  • file exists; tests the precondition; file is deleted during the test -> code is executed and exception is generated
  • file exists; tests the precondition; file is not deleted during the test -> code is executed and exception is not generated

are 3 possibilities and only in one of them the exception is generated, without the test, would be 2.

That’s design. Dealing with Try/catchs is design. The community (Java) has learned that clean code is important, that many lines of code are not as long as they favor design.

But, as I said, it depends. And it tends to generate much more speculation because it is not an exact thing but a good practice that varies according to the language and its community.

  • 2

    I liked the example of the date, in short you mean that "if the cost of the test is higher than the overhead of the exception, simpler to use the exception" (in addition to the code being cleaner, of course). As for the file, the problem I see is of DRY: my code to deal with the case where the file does not exist, I will have to repeat it both in the else how much in the catch? There are many cases where repetition is a sensible thing (e.g..: if(x) synchronized { if(x) { ... } } but in general I see value in keeping the code concise and expressive.

  • 4

    And in C# you use Datetime.Tryparse, no need to use an exception flow.

  • 2

    @Caffé Good counterpoint, I see that uniting an error code with an output parameter takes advantage over both approaches. Again, this is the kind of argument I’m looking for, I would hate for the excessive focus on the two cited languages to blind us to the consideration of a third alternative.

  • 2

    @mgipsonbr, more or less, but we can conclude that the order of relevance must be first the design and then the overhead - when this is not impactful. The Java language tells me (through the difference of checked and unverified exceptions) that I can recover from a Parseexception, treating. But that a Nullpointerexception will always be a programming error.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.