Error trapping with Try/Catch

Discussion in 'Teaching Programming with B4X' started by HotShoe, Sep 23, 2015.

  1. HotShoe

    HotShoe Well-Known Member Licensed User

    The Try/Catch combination is important to understand and use in your programs. Error detection and correction is the mark of good programming. It is not glamorous or even fun, but it is very necessary if you want to build a reputation for reliable, high quality apps and programs. The basic layout of a Try/Catch block looks like this :

    Code:
    Try
    ...normal code 
    to run goes in here.
    Catch
    ...code 
    to run if an error occurs goes here.
    End Try
    The way this block works is pretty simple and straight forward. The Try keyword tells B4X that an error trapping section is beginning here. All code between the Try and the Catch runs normally unless an error occurs inside THIS block of the app or program. If an error does occur, all processing after the Try keyword STOPS and the Catch part of the block is immediately run.

    You can run almost anything you like inside the Catch part of the block, but it should be geared toward dealing with the reason the error was raised. Sometimes you expect an error to happen and the alternative code is in your Catch block like this :

    Code:
    Sub getcfg
       
    Dim cfile As RandomAccessFile

       cfile.Initialize(
    File.DirInternal, "ADT.cfg"False)

       
    Try
       crec = cfile.ReadObject(
    0)
       
    Catch
       crec.dircolor = 
    Colors.Yellow
       crec.filebg = 
    Colors.Black
       crec.filefg = 
    Colors.White
       crec.linkcolor = 
    Colors.Green
       crec.markbg = 
    Colors.Yellow
       crec.markedcolor = 
    Colors.Magenta
       crec.markfg = 
    Colors.Black
       crec.startdir = curdir
       crec.infolblbg = 
    Colors.White
       crec.infolblfg = 
    Colors.Black
       crec.hidden = 
    False

       savecfg
       
    End Try

       cfile.Close

    End Sub
    The above sub expects there to be an error when the app is first installed. In that case, there will not be an ADT.cfg file since no configuration has ever been saved. When that happens, the Catch part of the block is run and default values are assigned to the config record type and then saved in the call to the savecfg sub.

    The Try/Catch below monitors a database transaction :

    Code:
    Sub Update_eng
       screen_to_erec 
    'assigns user input to the custom type erec.

       
    If erec.vtech Then
       vr= 
    1
       
    Else
       vr= 
    0
       
    End If

       
    If erec.ohc Then
       ohc= 
    1
       
    Else
       ohc= 
    0
       
    End If

       state1= 
    "Set ftype="&erec.fueltyp&",name='"&engname.Text&"',desc='"&engdesc.Text&"',cyl="&erec.cyl&",bore="&erec.bore&",stroke="&erec.stroke&",hgv="&erec.hgv&",ccv="&erec.ccv&",dish="&erec.dish&",ivdia="&erec.idia&",evdia="&erec.edia&",rocker="&erec.rock&",p2d="&erec.p2d&",vcyl="&erec.vcyl&",ecm="&erec.ecm&",intake="&erec.itype&",exhaust="&erec.extype&",heads="&erec.head&",nos="&erec.no2&",ltype="&erec.ltype&",spring="&erec.spress&","
       state2= 
    "ilift="&erec.ilift&",elift="&erec.elift&",idur="&erec.idur&",edur="&erec.edur&",icenter="&erec.icenter&",ecenter="&erec.ecenter&",boost="&erec.boost&",ccfm="&erec.blowcfm&",rods="&erec.rod&",balance="&erec.bal&",ign="&erec.ign&",blower="&erec.blower&",rodlength="&erec.rodlen&",fuelsys="&erec.carbtype&",vtec="&vr&"ohc="&ohc

       
    Try
       db.ExecNonQuery(
    "Update engines " & state1 & state2 & " Where id=" & cur_rec)
       
    ToastMessageShow("Record updated",True)
       
    Catch
       
    Msgbox("The following error occurred : "&LastException.Message,"Error")
       
    End Try

       do_eng
       dbpnl.Visible = 
    False
         dbpnl.SendToBack

       newrecord= 
    False
       engsavebtn.Enabled= 
    False
       hidedbscreen
       inengdb = 
    False

       show_main
    End Sub
    The code above attempts to update an sqlite db record. If all goes well, it displays a toast message saying that the record was saved, if not, it displays an error message in a msgbox. It does not take any corrective action, it just displays the error and then returns to the engine database input screen.

    Sometimes you want the app to notify you (the developer) in case of an error. In important operations that are critical to the function of the app, I use the sub below to give the user the choice of sending me an email containing the error that occurred and the device that it was running on at the time.

    Code:
    Sub ReportError(pName As String, Ver As String, inetAddress As String)
      
    Dim ex As ExceptionEx 'from the Threading library
      Dim error,ptype As String 'the error string and phone type info
      Dim mail As Email 'built in email type from Phone library

      ex.Initialize(
    LastException.Message)

      ptype = ph.Manufacturer & 
    " " & ph.Model & CRLF & pName & " version " & Ver & CRLF 'ph is of type Phone from the Phone library.
      error = ptype & CRLF & ex.ToString & CRLF & CRLF & ex.StackTrace & CRLF

      
    If Msgbox2("An error has occurred. The error is : "&LastException.Message&". Please report this error below.","ERROR","Report","","Dismiss",Null) = DialogResponse.POSITIVE Then
      mail.Attachments.Clear
      mail.Subject= pName & 
    " error" 'Example: pname = “Virtual Dyno”
      mail.To.Add(inetAddress)

      mail.Body= error
      
    StartActivity(mail.GetIntent) 'calls the intent to show the email client
      End If

    End Sub
    The reporterror sub above is called inside the Catch block and displays a msgbox that allows the user to either send me an error report or dismiss the error and continue. The sub does not hide anything from the user, it calls their email client and allows them to read the entire error message and requires them to press the Send button. Then the app attempts to deal with the error condition and continues to run normally (well, hopefully anyway).

    An example of when I use this is when critical calculations are being made or something is required to happen before the app can continue reliably. For example, I error check all EditTexts that a user can modify, but sometimes, (especially after a new update with lots of changes) I forget to check a new item for errors. If a routine expects a number and the value of an EditText (or any other type) is empty or is a string and not a number, an error is thrown.

    Here is a real usage of this type of error reporting from the Catch clause from my Virtual Dyno app :

    Code:
    Sub db_to_erec

    Try

      dbcur.Position= cur_pos

      dbname= u.proper(dbcur.GetString(
    "name"))
      dbdesc= u.proper(dbcur.GetString(
    "desc"))

      erec.cyl= dbcur.Getint(
    "cyl")
      erec.bore= dbcur.GetDouble(
    "bore")
      erec.stroke= dbcur.GetDouble(
    "stroke")
      erec.hgv= dbcur.GetDouble(
    "hgv")
      erec.p2d= dbcur.GetDouble(
    "p2d")
      erec.ccv= dbcur.GetDouble(
    "ccv")
      erec.dish= dbcur.GetInt(
    "dish")
      erec.vcyl= dbcur.GetInt(
    "vcyl")
      erec.idia= dbcur.GetDouble(
    "ivdia")
      erec.edia= dbcur.GetDouble(
    "evdia")
      erec.ilift= dbcur.GetDouble(
    "ilift")
      erec.elift= dbcur.GetDouble(
    "elift")
      erec.idur= dbcur.GetInt(
    "idur")
      erec.edur= dbcur.GetInt(
    "edur")
      erec.icenter= dbcur.GetInt(
    "icenter")
      erec.ecenter= dbcur.GetInt(
    "ecenter")
      erec.vtech= dbcur.GetInt(
    "vtec") > 0
      erec.ltype= dbcur.GetInt(
    "ltype")
      erec.rock= dbcur.GetDouble(
    "rocker")
      erec.spress= dbcur.GetInt(
    "spring")
      erec.extype= dbcur.GetInt(
    "exhaust")
      erec.head= dbcur.GetInt(
    "heads")
      erec.rod= dbcur.GetInt(
    "rods")
      erec.bal= dbcur.GetInt(
    "balance")
      erec.ign= dbcur.GetInt(
    "ign")
      erec.itype= dbcur.GetInt(
    "intake")
      erec.no2= dbcur.GetInt(
    "nos")
      erec.boost= dbcur.GetInt(
    "boost")
      erec.rock= dbcur.GetDouble(
    "rocker")
      erec.blower= dbcur.GetInt(
    "blower")
      erec.blowcfm= dbcur.GetInt(
    "ccfm")
      erec.rodlen= dbcur.GetDouble(
    "rodlength")
      erec.fueltyp= dbcur.GetInt(
    "ftype")
      erec.carbtype= dbcur.GetInt(
    "fuelsys")
      erec.ohc = dbcur.GetInt(
    "ohc") > 0
      erec.ecm= dbcur.GetInt(
    "ecm")

      erec_to_vrec 
    'converts eng db record to a vehicle record

    Catch
    reporterror
    Init_All

    End Try

    dbcur.Close

    End Sub
    The sub above needs to succeed in getting a database record loaded correctly into the custom type erec before the app can calculate the results and display reliable values. The sub that actually does the calculations is also enclosed in a Try/Catch block to ensure that errors are trapped and then all values are reset to known defaults if an error occurs.

    The Try/Catch block allows you to save face to your users in the event an error occurs. It looks more professional to tell the user "Yeah I know an error occurred and I want to know why and where." instead of something like "An error has occurred and this app will be closed", or you can handle the error silently and put the app into a condition that it can continue running reliably.

    If you want to know why some apps get tens of millions of downloads and consistently high ratings, it's because they do what they are supposed to do without crashing in an ugly, gory manner. Error checking and correcting is just part of the game in professional programming, regardless of whether it is a free or paid app. Check user inputs and especially any file operations done in your apps or programs and the world will be a much happier place.

    I know that sounds a bit like I'm preaching, but it is one of my pet peeves that many if not all new programmers do. They get the guts in the app to make it run, but leave out the tedious part of going through all of the possible problems that can come up. Learning to think like a machine is a very handy talent to acquire in the programming world, and it just takes time and experience to get there. The effort is well worth your time though.

    --- Jem
     
    Last edited: Sep 23, 2015
    Harris, Peter Simpson, BPak and 10 others like this.
  2. Informatix

    Informatix Expert Licensed User

    Please note that the exceptions thrown in a sub called by CallSub will not be catched by the Try/Catch around CallSub because CallSub raises an event that does not propagate exceptions to the caller.
     
    pesquera likes this.
  3. Cableguy

    Cableguy Expert Licensed User

    Hi guys....
    I thought of posting here instead of Q&A section, as it can help other understand, that like me, had never really used a try:catch block before...

    I have this code:
    Code:
    Dim durationofmonth As Long 
            
    For n = 27 To 31
                
    Try
                    durationofmonth = 
    DateTime.GetDayOfMonth(DateTime.DateParse(n & "/" & DateTime.GetMonth(ticks) & "/" & DateTime.GetYear(ticks)))
                
    Catch
                    
    Log(LastException)
                
    End Try
            
    Next
    My basic thought was, in a for/next, to increase the testing date by one day until it errors out, thus finding the duration of the given month ( the mont is passed to the sub in ticks).
    It works quite nicely in release mode, and the execution is not stopped, BUT, in debug, the exception stops the execution... I thought the debug mode would manage the try/catch and let the exception be dealt by the code without stopping...
    Is this the normal debug behaviour for a try/catch?


    [EDIT] when using a try/catch inside a loop, make sure to exit the loop on the catch section or the exception will halt execution!

    [EDIT 2] I can't get the exception to be ignored by the debugger, even after exiting the loop in the catch section, any thoughts?
     
    Last edited: Jan 1, 2016
  4. stevel05

    stevel05 Expert Licensed User

    This works for me:
    Code:
    DateTime.DateFormat = "dd/MM/yyyy"
        
    Dim Ticks As Long = DateTime.DateParse("27/02/2015")
        
    Dim durationofmonth As Long
        
    For n = 27 To 31
            
    Try
                durationofmonth = 
    DateTime.GetDayOfMonth(DateTime.DateParse(n & "/" & DateTime.GetMonth(Ticks) & "/" & DateTime.GetYear(Ticks)))
            
    Catch
                
    Log(LastException)
                
    Exit
            
    End Try
        
    Next
        
    Log(durationofmonth)
     
  5. stevel05

    stevel05 Expert Licensed User

    This also works and uses less processing if you can format the date differently:

    Code:
    DateTime.DateFormat = "dd/MM/yyyy"
        
    Dim Day As String
        
    Dim Month As String = "02"
        
    Dim Year As String = "2015"
        
    For n = 27 To 31
            Day = n
            
    Try
                
    DateTime.DateParse(Day & "/" & Month & "/" & Year)
            
    Catch
                
    Log(LastException)
                Day = n-
    1
                
    Exit
            
    End Try
        
    Next
        
    Log("Last valid day = " & Day)
    They both fail if you change the code and don't clean the project before running again in the rapid debugger.
     
    Last edited: Jan 1, 2016
  6. Cableguy

    Cableguy Expert Licensed User

    I may not have been clear making my point....
    The routine works as expected, and I retrieve the correct durationofmonth value.
    My issue is, why, in debug mode, the execution halts when the exception is caught?
    I will try to clean the project and see how it goes...

    [EDIT] Cleaning the project solved my problem, the loop exits when the exception is catched and thus I get the correct value for my variable
     
  7. PCowling

    PCowling Member Licensed User

    Thanks for the mini tutorial. This is an important area of teaching and it really helps. Thank you.
     
Loading...