4

I had a discussion the other day: https://stackoverflow.com/a/42156860/937125 where I didn't quite understand why an Abort was better than calling Exit in that situation. I tend not to use it in my code flow. I consider it a bad practice and bad for code flow. but @David's statement in the comments made me wonder if maybe I was missing something:

Without a silent exception, how would you abort an operation when deep down the call stack. For instance how would you abort a file copy operation with a 10 deep call stack? Isn't that exactly what exceptions are designed for? Sure you can code it without exceptions but it is much more verbose and error prone.

I can't imagine such situation. Can someone give me an example of such code/scenario, and convince me that Abort in the above case is really a good thing and "much more verbose and error prone". (3-4 deep call stack is enough to illustrate)

Community
  • 1
  • 1
kobik
  • 21,001
  • 4
  • 61
  • 121
  • I really sympathize with you, as you saw that I am the one who suggested the Exit function, and David mentioned that....."What happens is that you sometimes extract the code into another method, and then Exit only bails out of that method. Abort is robust to such changes", and this did not put more light on me – Alec Feb 15 '17 at 06:37
  • @Fero Method A calls method B calls method C. Method C does an early exit. But then B needs to do the same. And so does A. – David Heffernan Feb 15 '17 at 06:47
  • Ok but that would only depend what is your method doing? because if your method does not return anything then I dont see a problem... – Alec Feb 15 '17 at 06:51
  • 1
    @David, sorry if I would be asking too much, but are you able to answer this question with a Minimal, Complete, and Verifiable example of why is Abort better than Exit, I think this would share more light on everyone – Alec Feb 15 '17 at 06:58
  • @Fero whether or not the method returns values isn't the point. What matters is if A or B attempt to perform actions after C returns. Once the user has cancelled you don't want those actions to be performed. How are you going to stop it. – David Heffernan Feb 15 '17 at 07:00
  • As an example of exception based code vs error code based code consider the code from this question http://stackoverflow.com/questions/14584838/how-to-create-a-lossless-jpg-using-wic-in-delphi and then this answer http://stackoverflow.com/a/42228537/505088 – David Heffernan Feb 15 '17 at 07:03
  • If you visit the documentation for Abort it refers to this code sample http://docwiki.embarcadero.com/CodeExamples/en/BeforePost_(Delphi) – David Heffernan Feb 15 '17 at 07:26
  • 2
    @DavidHeffernan, I realize the difference between "exception based code vs error code based code". I'm not sure how this is related to the `Abort`, specially the example of copying files and canceling it 10 deep in the stack. I understand the example of the `BeforePost` although I would not used such code. What's the dataset state after you call `Abort` on the `OnBeforePost`? what is the state of the transaction, if started before posting? It depends on the implementation behind the scene and you need to know the inside-out of it to be safe/sure to use it with an `Abort` in that case. – kobik Feb 15 '17 at 07:54
  • OK. Fine. Feel free to carry on the way you are doing. What are you missing out on? – David Heffernan Feb 15 '17 at 07:56
  • 2
    "OK. Fine. Feel free to carry on the way you are doing." I'm not here to **argue**. I'm here to learn, because I really feel I'm missing the point. so can you please be patient and show a MCVE for a *"For instance how would you abort a file copy operation with a 10 deep call stack?"* where an `Abort` is a better solution. – kobik Feb 15 '17 at 08:03
  • My first comment to this post explains the scenario. You really want to return a boolean from every function in the call stack and test and exit whenever you call such a function? I can write it out for you when I get to a computer but I don't think you will be surprised. – David Heffernan Feb 15 '17 at 08:15
  • @DavidHeffernan, You refer to *"Method A calls method B calls method C. Method C does an early exit. But then B needs to do the same. And so does A"*? You mean you call `Abort` in method `C` to cancel the entire operation? – kobik Feb 15 '17 at 08:24
  • @kobik, Just see the abort as exception, why raise an exception and not exit ? Depends on the case, you don't care about the stack that called that method once this part of code failed, so you raise an exception. Imagine that you have a method Print, in this method that calls Print, you treat all the exception that can happens like, out of paper, lost connection with printer, etc. So instead of call exit in all nested methods and check for it result, you raise an exception and let someone treat it. I'm not a big fan of the abort, because it's very generic for me.... – Rodrigo Farias Rezino Feb 15 '17 at 08:37
  • Correct, that's what I mean. – David Heffernan Feb 15 '17 at 08:39

2 Answers2

7

The simplest scenario that illustrates my point is like so:

procedure MethodA;
begin
  MethodB;
  MethodC;
end;    

procedure MethodB;
begin
  // ... do stuff
end;

procedure MethodC;
begin
  // ... do stuff
end;

That's fine as it is. Now suppose that MethodB asks the user for some input, and if the user presses the Cancel button, that no further work should be carried out. You could implement that like this:

procedure MethodA;
begin
  if MethodB then
    MethodC;
end;    

function MethodB: Boolean;
begin
  Result := MessageDlg(...)=mrOK;
  if not Result then
    exit;
  // ... do stuff
end;

procedure MethodC;
begin
  // ... do stuff
end;

That works fine, but imagine that you in the real world code, there was deeper nesting. The boolean returned by MethodB might need to be passed on up a great many levels. This would become cumbersome.

Or consider what happens if MethodB needs to return a value to its caller. In that scenario the original code might be like so:

procedure MethodA;
begin
  MethodC(MethodB);
end;    

function MethodB: string;
begin
  Result := ...;
end;

procedure MethodC(Value: string);
begin
  // ... do stuff with Value
end;

Now once more consider what happens if the user gets a chance to cancel. How can we return both a boolean and a string from MethodB? Using an out parameter for one of the return values? Using a compound structure like a record to wrap both values. The latter obviously involves lots of boilerplate so let us explore the former.

procedure MethodA;
var
  Value: string;
begin
  if MethodB(Value) then
    MethodC(Value);
end;    

function MethodB(out Value: string): Boolean;
begin
  Result := MessageDlg(...)=mrOK;
  if not Result then
    exit;
  Value := ...;
end;

procedure MethodC(Value: string);
begin
  // ... do stuff with Value
end;

For sure you can do this, but this is beginning to look like the sort of code that exceptions were designed to simplify. And at this point, let us consider the existence of a silent exception, EAbort, raised by calling Abort, that does not result in a message being shown by the top level exception handler. That last point is what is meant by silent.

Now the code becomes:

procedure MethodA;
begin
  MethodC(MethodB);
end;    

function MethodB: string;
begin
  if MessageDlg(...)<>mrOK then
    Abort;
  Result := ...;
end;

procedure MethodC(Value: string);
begin
  // ... do stuff with Value
end;

The advantage is that MethodA does not need to worry about cancellation. And if the call stack was deeper, none of the methods between MethodA at the top, and MethodB at the point of user input, would need to know anything about cancellation.

A further benefit is that MethodB can retain its natural signature. It returns a string. In case of failure, either from a more traditional exception, or from user cancellation, an exception is thrown.

This very simple example isn't that much more compelling than the previous one that does not use Abort. But imagine what the code would look like if MethodB were 4 or 5 deep in the call stack?


I am absolutely not saying that Abort should always be used in place of exit. My belief is that both have their place. Where Abort shines is when the user opts to cancel an operation and you don't want any more processing to take place in the current event handler. Furthermore, since the user expressly opted to cancel, no further UI needs to be presented to them. You don't need a message box telling the user that they cancelled, they already know that.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    Sorry, David, this leaves me even more confused. What is to stop the silent exception rippling up even further? (I know you said MethodA is top level, but in practice it need not be). Don't you need a try except construct at the top level that you want to break out to? Maybe not in this artificial example but it would surely make thinks clearer? – Dsm Feb 15 '17 at 10:03
  • @Dsm In the VCL framework, there is a top level exception handler that catches and ignores `EAbort`. Obviously you need there to be something to do that. The entire concept of exceptions falls over if you cannot trust exceptions to be caught and handled at the right point. – David Heffernan Feb 15 '17 at 10:18
  • Yes David, I know that. I think you are missing my point. If MethodA is where you want it to stop and MethodA is halfway up the call stack, what is to stop it going all the way back to the VCL framework? I simply felt that it could be clearer. – Dsm Feb 15 '17 at 12:48
  • @Dsm I'm assuming that the reader understand how exceptions work. I honestly don't feel that it is appropriate to explain that in detail. Are you still confused? – David Heffernan Feb 15 '17 at 12:52
  • No, not confused. Nor did I want you to explain in any greater detail than you did. Just thought a try except clause in your sample code would make things clearer to future readers. It is easier to understand something that is there rather than something that is missing especially when skim-reading. – Dsm Feb 15 '17 at 13:24
  • @Dsm Actually, I think it's a common mistake to use except where it's not needed. Usually for user interaction you want this to float on up. – David Heffernan Feb 15 '17 at 13:29
4

Assume your program is doing a lengthy operation either in a separate thread or (even though it's frowned upon) calling Application.ProcessMessages. Now, you want the user to be able to abort that operation in a safe manner (that is: All resources are cleaned up, the data is in a consistent state etc.). So, the UI sets a flag somewhere and in your code you periodically check for that flag. If it is set, you call Abort or explicitly raise EAbort. This will cause all your carefully crafted try / except / finally blocks to be execute and making sure aborting the operation is safe.

// in the main thread:
procedure TMyProgressDialog.b_AbortClick(Sender: TObject);
begin
  if AskUserIfHeIsSure then begin
    gblAbortedFlag := true;
    b_Abort.Enabled := false;
    b_Abort.Caption := _('Aborting');
  end;
end;

// call this repeatedly during the lenghty operation:
procecdure CheckAborted;
begin
  // If you are in the main thread, you might want to call
  // Application.ProcessMessages;
  // here. If not, definitely don't.
  if gblAbortedFlag then
    Abort;
end;

Of course this could be done with a different exception, but I can't think of any other way to safely exit from a deep call stack without having to program lots of ifs and exits.

dummzeuch
  • 10,975
  • 4
  • 51
  • 158