Recent

Author Topic: Can try...except blocks be spanned across procedures ?  (Read 5514 times)

Nitorami

  • Sr. Member
  • ****
  • Posts: 396
Can try...except blocks be spanned across procedures ?
« on: March 12, 2015, 09:49:40 pm »
My console program has a global procedure IntError(msg: string) which is called on any error. It simply writes msg to the console and halts. It does not clean up before, and leaves a lot of unfreed memory blocks.

I now changed IntError to raise an exception:

Code: [Select]
procedure IntError (Msg: string);
begin
//  writeln (msg); halt;
  Raise Exception.Create (Msg) at get_caller_addr (get_frame);
  writeln ('Exception raised !'); //for testing only
end;

The main program now looks like

Code: [Select]
Main := TScheduler.Create;
with Main do begin
  try
   (A large number of objects are created here. In case anyone causes an error, it calls IntError)
  except
    on E: Exception do begin
       writeln (E.Message);
       Main.Destroy (destroys all objects and itself)
       halt;
    end;
end;

That works fine in principle.

But: I found that after having raised the exception, the program instantly jumps to the except block, without finishing IntError neither returning to the method that called IntError. How can that work without stack corruption ? It it legal to use try... except blocks in this way, across procedures ?

Mike.Cornflake

  • Hero Member
  • *****
  • Posts: 1251
Re: Can try...except blocks be spanned across procedures ?
« Reply #1 on: March 12, 2015, 10:24:12 pm »
Legal, but not wise.

Consider...

Code: [Select]
  try
    A := TA.create;
    B := TB.Create;
    {code that raises an exception}
    C := TC.create;
  Except
    {Code that uses object A}
    {Code that uses object B}
    {Code that uses object C}
  End;

The code in your Except block is going to raise a further error when you try to use object C, as that object NEVER GOT CREATED.   By putting your Try/Except block over such a wide range of code, you put your self in a position that you have no idea what you can trust at the point the exception is raised.  Sure, after 5 second thought I can see two ways to resolve this particular example,  but that's not the point.  I'm sure there's a clever term for it, but I always try to keep each individual problem as small as possible.  If I could, I'd try to stick multiple Try/Except.  Something along the line of...

Code: [Select]
  try
    A := TA.create;
    try
      {Code Block 1}
      B := TB.Create;
      try
        {Code Block 2}
        C := TC.create;
        Try
           {Code Block 3}
        Except
            {Here you know A, B & C has been created, and you know the exception was in Code Block 3}
        End;
      Except
          {Here you know A & B has been created, and you know the exception was in Code Block 2}
      End;
    Except
        {Here you know only A has been created, and you know the exception was in Code Block 1}
    End;

And no, I'm not saying I write all my code like that :-)  I'm just trying to get to you to think in terms of smaller Error handling cases instead of Global handling cases.

And another thing :-)

Don't free your objects in an Except block.  If you do that, you run the risk of them ONLY being free'd if an exception is raised.  You want a Try Finally Block for freeing objects;

Code: [Select]
A := TA.Create;
Try
  {Code Block that uses A}
Finally
  A.Free;
End;

This way, even if an exception is created inside the code block, object A will still be free'd...

Hope this helps

Mike
Lazarus Trunk/FPC Trunk on Windows [7, 10]
  Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
  BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

Mike.Cornflake

  • Hero Member
  • *****
  • Posts: 1251
Re: Can try...except blocks be spanned across procedures ?
« Reply #2 on: March 12, 2015, 10:47:22 pm »
Another thing to consider.

Exceptions are expensive (time) - which is why my code doesn't actually look like the above. 

It's far cheaper to ensure that exception are never raised.  About to divide by a variable?  Better check it's not zero first.  Converting a string to a number?  Better check that string is the correct sort of number first...  (There's a slew of functions designed to help you with this: StrToIntDef(), StrToFloatDef() and so on)

I only really deal with exceptions when I'm using objects that raise exceptions beyond my control (DB Controls).

I subscribe to a coding policy called Defensive Programming.  I've had this discussion on here before, and got shot down quickly, loudly and often :-)   However, I still use it, and I still think it's the best policy for beginners...
Lazarus Trunk/FPC Trunk on Windows [7, 10]
  Have you tried searching this forum or the wiki?:   http://wiki.lazarus.freepascal.org/Alternative_Main_Page
  BOOKS! (Free and otherwise): http://wiki.lazarus.freepascal.org/Pascal_and_Lazarus_Books_and_Magazines

Bart

  • Hero Member
  • *****
  • Posts: 4225
    • Bart en Mariska's Webstek
Re: Can try...except blocks be spanned across procedures ?
« Reply #3 on: March 13, 2015, 12:08:59 pm »
Code: [Select]
procedure IntError (Msg: string);
begin
//  writeln (msg); halt;
  Raise Exception.Create (Msg) at get_caller_addr (get_frame);
  writeln ('Exception raised !'); //for testing only
end;

You are aware that the writeln statement will never be executed?

Bart

marcov

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 9229
  • FPC developer.
Re: Can try...except blocks be spanned across procedures ?
« Reply #4 on: March 13, 2015, 12:14:29 pm »
I only really deal with exceptions when I'm using objects that raise exceptions beyond my control (DB Controls).

Everything that leads to a memory allocation can potentially raise EOutofMemory.

Nitorami

  • Sr. Member
  • ****
  • Posts: 396
Re: Can try...except blocks be spanned across procedures ?
« Reply #5 on: March 13, 2015, 06:53:49 pm »
All

Thank you for all the hints, I can really use them as I've not used exceptions before.
@Mike: I understand your caveats about speed and keeping the blocks as small as possible. I am making a Monte Carlo simulation for fault tree analysis, and am using exceptions only for the initialisation part where speed does not matter. Currently I need to program the problem directly in the code; I may consider a parser or a GUI later. I have constructed a syntax which should make parsing relatively easy, as it largely accepts strings as parameters.

My toolbox offers a Class TScheduler which encapsulates the whole simulation. The simulation problem is defined by declaring various nodes and connections between them via methods in TScheduler, which holds the nodes in a list. It may look like

Code: [Select]
Main := TScheduler.Create;
with Main do begin
  try //start user defineable section
    CompE ('Lamp', 20, 300);
    CompE ('Switch', 1000);
    Period   ('W1'. 80000);
    Connect ('Lamp', 'Switch');
    ....
   //end user defineable section
  except
   on E: Exception do begin
     writeln (E.message);
     Main.Destroy;
     halt;
   end;
 end;
end; //with main

 Main.run;
 Main.Destroy;
end.

Most of the individual statements create a specific class instance, but are otherwise independent. Therefore I think it is manageable to raise an exception e.g. in case I entered a nonsense parameter, and let the exception destroy the list and halt. The purpose is for learning really, as a useable program would not work like that but at least offer a parser, where error handling could be done differently.
Well, this works but leaves a few unfreed memory blocks on termination and I cannot find them. Now:

@Bart: Yes I am aware that the code following the raise statement in IntError is never executed, and that brings me back to the original question: By raising an exception, the program jumps directly to the exception statement, and does not even return to the functions that called it. How does the stack survive this ? Who handles the stack in such a case ? The OS? Or does this cause a stack problem which is then the reason for the unfreed memory blocks ?


Nitorami

  • Sr. Member
  • ****
  • Posts: 396
Re: Can try...except blocks be spanned across procedures ?
« Reply #6 on: March 15, 2015, 11:12:19 am »
I have stripped this down to the simple code below. Heaptrc still reports 3 unfreed blocks.... why ?

Code: [Select]
{$mode objfpc}
uses heaptrc, sysutils;
{---------------------------------------------------------------------}

type TNode = Class
       constructor Create;
     end;


type TScheduler = Class
       ThisNode : TNode;
       constructor Create;
       destructor  Destroy;
       procedure   NewNode;
     end;

(* TNode *)

constructor TNode.Create;
begin
  Inherited Create;
  Raise Exception.Create ('TNode.Create raised an exception');
end;


(* TScheduler *)

constructor TScheduler.Create;
begin
  Inherited;
end;

procedure TScheduler.NewNode;
begin
  ThisNode := TNode.Create;
end;

destructor TScheduler.Destroy;
begin
  writeln ('-----------------------------');
  writeln ('Start TScheduler.Destroy');
  FreeAndNil (ThisNode);
  Inherited;
  writeln ('Finish TScheduler.Destroy');
  writeln ('-----------------------------');
end;


var Main: TScheduler;
begin
  try
    Main := TScheduler.Create;
    Main.NewNode;
  except
    on E: Exception do begin
      writeln ('-----------------------------');
      writeln (E.message);
      writeln ('-----------------------------');
      Main.Destroy;
      halt;
    end;
 end;

 writeln ('ok');
 Main.Destroy;
end.


wp

  • Hero Member
  • *****
  • Posts: 8307
Re: Can try...except blocks be spanned across procedures ?
« Reply #7 on: March 15, 2015, 11:42:49 am »
Don't know why your solution does not work. But its not the way I would do it anyway. Use the try-finally construct to release resources  because the "finally" section is always reached even in case of an "exit".

Code: [Select]
  try
    try
      Main := TScheduler.Create;
      Main.NewNode;
      WriteLn('ok');
    except on E:Exception do
      WriteLn(E.Message);
    end;
  finally
    Main.Destroy;
  end;
Mainly Lazarus trunk / fpc 3.2.0 / all 32-bit on Win-10, but many more...

Nitorami

  • Sr. Member
  • ****
  • Posts: 396
Re: Can try...except blocks be spanned across procedures ?
« Reply #8 on: March 15, 2015, 12:19:34 pm »
Yes I see. That works. I try to finally get it  :)

My main mistake was to halt within an except block, which apparently interrupts the housekeeping required for the exception handling and hence generates memory leaks.

ausdigi

  • Jr. Member
  • **
  • Posts: 52
  • Programmer
    • RNR
Re: Can try...except blocks be spanned across procedures ?
« Reply #9 on: March 30, 2015, 07:58:56 am »
I just saw wp's code and thought I should correct it:

OLD CODE
Code: [Select]
  try
    try
      Main := TScheduler.Create; { what happens if this raises an exception?!? }
      Main.NewNode;
      WriteLn('ok');
    except on E:Exception do
      WriteLn(E.Message);
    end;
  finally
    Main.Destroy;
  end;

NEW CODE
Code: [Select]
  try
    Main := TScheduler.Create;
    try { only try and finally free it AFTER its been successfully created }
      Main.NewNode;
      WriteLn('ok');
    finally
      Main.Destroy; { or .Free? }
    end;
  except on E:Exception do
    WriteLn(E.Message);
  end;
Win10/64: CT 5.7 32&64

 

TinyPortal © 2005-2018