Search code examples
delphiabort

Using Abort to improve/simplify code in some situations


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)


Solution

  • 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.