Show/Hide Toolbars

XSharp

Navigation: X# Documentation > X# Tips and Tricks

Catching runtime errors at startup

Scroll Prev Top Next More

Sometimes your program throws runtime errors at startup. These can be caused by missing assemblies and or by errors in initialization code.
These errors can be difficult to trap, since the errors occur inside code that is executed before the first line of code in your application.

Take the following example

GLOBAL x AS INT
GLOBAL y := 1 / x AS INT
 
FUNCTION Start AS VOID
? "Function Start"
  RETURN

This code will generate an exception at startup, which you can only see/read when you run the app from the commandline

Unhandled Exception: System.TypeInitializationException: The type initializer for 'Application1.Exe.Functions' threw an exception. ---> System.DivideByZeroException: Attempted to divide by zero.
at Application1.Exe.Functions..cctor() in C:\XIDE\Projects\Default\Applications\Application1\Prg\Start.prg:line 4
--- End of inner exception stack trace ---
at Application1.Exe.Functions.Start()

The normal program flow in an X# application is this:

Your application is started

The DotNet framework is initialized

The entrypoint is called. The normal entrypoint in an app is the Start function, which is converted by the compiler to a Start method in a compiler generated Functions class.
The same class also has the globals and defines from your app. If one of these globals or defines contains an initialization expression that cannot be resolved at compile then these this code will be executed in the static constructor of the Functions class (in the error message above this is called the "type initializer").

The code above clearly makes a mistake which causes a divide by zero error.

 

To intercept this we would like to run some other code at startup and add a try .. catch construct to make sure we can catch this kind of errors.

 

Add the following code:

CLASS MyStartupCode
STATIC METHOD Start AS VOID STRICT
TRY
  // Note that in the following line the name before .Exe must
  // match the file name of your EXE. In my case I am generating Application1.exe
  Application1.Exe.Functions.Start()
CATCH e AS Exception  
  // We should probably log this to disk as well !
  Console.WriteLine("An unhandled exception has occurred")
  Console.WriteLine("===================================")
  DO WHILE e != NULL        
    Console.WriteLine("Exception: "+e:Message)                            
    Console.WriteLine("Callstack:")
    Console.WriteLine(e:StackTrace)
    Console.WriteLine()
    e := e:InnerException
  ENDDO            
  Console.WriteLine("===================================")
  Console.WriteLine("Press any to close the application")
  Console.ReadLine()  
END TRY
RETURN
END CLASS

 

You may have to change the call to Application1.Exe.Functions.Start() into something that matches your EXE name.

Now goto the General page in the application properties in VS and at the entry "Startup Object" set the value MyStartupCode.

 

StartupCode

 

In XIDE add the command line option -main:MyStartupCode:

 

XideStartup

 

and run the code again.
The error is now trapped and shown.
If you app is not a Console app but a Windows app then the console output may not be visible.

 

Of course you can also register an UnHandledException handler in the AppDomain class inside the new startup code.

Change the code to:

CLASS MyStartupCode
STATIC METHOD Start AS VOID STRICT        
TRY
  System.AppDomain.CurrentDomain:UnhandledException += ExceptionHandler
  Application1.Exe.Functions.Start()
CATCH e AS Exception
  ExceptionHandler(NULL, UnhandledExceptionEventArgs{e, TRUE})
END TRY
RETURN
 
STATIC METHOD ExceptionHandler( sender AS OBJECT, args AS UnhandledExceptionEventArgs) AS VOID
  LOCAL e AS Exception                  
  LOCAL c AS STRINGe := (Exception) args:ExceptionObject
   c := "An unhandled exception has occurred"+crlf
   c += "==================================="+crlf
  DO WHILE e != NULL
      c += "Exception: "+e:Message+crlf
      c += "Callstack:"+crlf
      c += e:StackTrace+crlf
      e := e:InnerException
  ENDDO
   c += "==================================="+crlf      
 MessageBox(IntPtr.Zero, c,"Error",0x60010) // MB_OK + MB_ICONSTOP+ MB_DEFAULT_DESKTOP_ONLY + MB_TOPMOST
 
[DllImport("user32.dll", CharSet := CharSet.Ansi)];
STATIC METHOD MessageBox(hwnd AS IntPtr, lpText AS STRING, lpCaption AS STRING, uType AS DWORD) AS INT PASCAL  
END CLASS

 

One remark:

Do NOT use or call any Xbase types and or functions in the exception handler, since you can't be sure that the runtime was initialized properly.

If you use classes written by yourself make sure that everything is strongly typed and uses native types only. So no USUAL, FLOAT, SYMBOL etc.

And do not call code inside functions in the same app or DLLs, because again the type initializers for the classes in which these functions are located can also throw exceptions.