views:

312

answers:

7

Hi All,

Just wondering if this is considered a clear use of goto in C#:

IDatabase database = null;

LoadDatabase:
try
{
 database = databaseLoader.LoadDatabase();
}
catch(DatabaseLoaderException e)
{
 var connector = _userInteractor.GetDatabaseConnector();
 if(connector == null)
  throw new ConfigException("Could not load the database specified in your config file.");
 databaseLoader = DatabaseLoaderFacade.GetDatabaseLoader(connector);
 goto LoadDatabase;
}

I feel like this is ok, because the snippet is small and should make sense. Is there another way people usually recover from errors like this when you want to retry the operation after handling the exception?

Edit: That was fast. To answer a few questions and clarify things a bit - this is part of a process which is essentially converting from a different kind of project. The _userInteractor.GetDatabaseConnector() call is the part which will determine if the user wants to retry (possibly with a different database than the one in the config they are loading from). If it returns null, then no new database connection was specified and the operation should fail completely.

I have no idea why I didn't think of using a while loop. It must be getting too close to 5pm.

Edit 2: I had a look at the LoadDatabase() method, and it will throw a DatabaseLoaderException if it fails. I've updated the code above to catch that exception instead of Exception.

Edit 3: The general consensus seems to be that

  • Using goto here is not necessary - a while loop will do just fine.
  • Using exceptions like this is not a good idea - I'm not sure what to replace it with though.
+4  A: 

Personally, I would have this in a separate method that returns a status code of success or failure. Then, in the code that would call this method, I can have some magic number of times that I would keep trying this until the status code is "Success". I just don't like using try/catch for control flow.

BFree
The magic number of times is critical: the OP's code would do an infinite loop if the error persists (which is probably pretty likely if an error happens at all).
John Zwinck
+1 very "clean" solution.
PRR
You can wrap your check to a magic number around an if statement, and if your magic number is 0 or -1, then don't bother to ever stop the loop until you get a Success.
Thomas Owens
+15  A: 

Is there another way people usually recover from errors like this when you want to retry the operation after handling the exception?

Yes, in the calling code. Let the caller of this method decide if they need to retry the logic or not.

UPDATE:

To clarify, you should only catch exceptions if you can actually handle them. Your code basically says:

"I have no idea what happened, but whatever I did caused everything to blow up... so lets do it again."

Catch specific errors that you can recover from, and let the rest bubble up to the next layer to be handled. Any exceptions that make it all the way to the top represent true bugs at that point.

UPDATE 2:

Ok, so rather than continue a rather lengthy discussion via the comments I will elaborate with a semi-pseudo code example.

The general idea is that you just need to restructure the code in order to perform tests, and handle the user experience a little better.

//The main thread might look something like this

try{
    var database = LoadDatabaseFromUserInput();

    //Do other stuff with database
}
catch(Exception ex){
    //Since this is probably the highest layer,
    // then we have no clue what just happened
    Logger.Critical(ex);
    DisplayTheIHaveNoIdeaWhatJustHappenedAndAmGoingToCrashNowMessageToTheUser(ex);
}

//And here is the implementation

public IDatabase LoadDatabaseFromUserInput(){

    IDatabase database = null;
    userHasGivenUpAndQuit = false;

    //Do looping close to the control (in this case the user)
    do{
     try{
      //Wait for user input
      GetUserInput();

      //Check user input for validity
      CheckConfigFile();
      CheckDatabaseConnection();

      //This line shouldn't fail, but if it does we are
      // going to let it bubble up to the next layer because
      // we don't know what just happened
      database = LoadDatabaseFromSettings();
     }
     catch(ConfigFileException ex){
      Logger.Warning(ex);
      DisplayUserFriendlyMessage(ex);
     }
     catch(CouldNotConnectToDatabaseException ex){
      Logger.Warning(ex);
      DisplayUserFriendlyMessage(ex);
     }
     finally{
      //Clean up any resources here
     }
    }while(database != null); 
}

Now obviously I have no idea what your application is trying to do, and this is most certainly not a production example. Hopefully you get the general idea. Restructure the program so you can avoid any unnecessary breaks in application flow.

Cheers, Josh

Josh
This would be my normal approach, but this is part of a batch process of sorts so I can't just throw and expect a retry. The database connector that it starts with is retrieved from a config file, the error handling is giving the user a chance to recover from an error in that config. I have replaced the `catch(Exception e)` with `catch(DatabaseLoaderException e)` to make it more specific.
Jamie Penney
@Jamie: Neither can you catch, retry and expect a successful result immediately - the loop may very well be endless if an exception is occurring. Log the error, even send it by email. If you just throw the error and abort, the next time the batch is run it will still try again, but later - there is more of a chance it will work later (having had time to resolve errors in between runs) instead of a continuous loop on an error.
John K
Have a look at my edit above, I mentioned this. The loop will end once the user stops entering new database connection information -> they can cancel it at any stage. _userInteractor is a interface to the UI, and will return null if the user cancels. So it won't be a continuous error.
Jamie Penney
I think you need to refactor your code some. The problem is that you are using exceptions to manage application flow. Exceptions should be... well... exceptional. If you are expecting something to happen, then you should test for that in your code. Try rearranging things so you don't have to wrap all that logic in a catch.
Josh
What qualifies as exceptional? The database loader will throw if it can't load from the database, and the exception holds information about the error. This can be anything from an error connecting to the database, to an error in the schema. I could catch particular exception subtypes, and do something different based on each exception, but at the moment I was just going to report the exception information to the user and let them decide if they want to change databases or cancel and fix things.
Jamie Penney
Exactly my point though. If the user is truly in control of what is going on here, then they ought to be notified of what is going on. Think of SQL Management studio, or the Visual Studio Server Explorer. What happens when the connection to the database fails? The message bubbles all the way up to the UI layer, and THEN the user gets to decide how to proceed. I am not saying your code is fundamentally broken or anything, but you just need to rearange it so the caller with the most information can make a decision about how to handle the exception. Don't hide that information.
Josh
I see what you mean now. That makes a lot more sense than what I was thinking. Thanks.
Jamie Penney
Sometimes it can be hard to see the forest for the trees ;) It is always good to get someone who is not close to the problem domain to look over your code.
Josh
+1  A: 

No, it's not okay: http://xkcd.com/292/

Greg Hewgill
Making blanket statements about gotos is as bad as using them indiscriminately. Goto's are an advanced programming construct to be used only by advanced programmers to simplify method structure. The use in question by the OP is not appropriate, thats's for sure (it's been answered why already), but then saying gotos are always bad is bad too.
gmagana
Hmmm... Argumentum ad XKCD is an acceptable and excellent fallacy!
MPelletier
Funny, but not actually helpful. -1
Oorang
+7  A: 

maybe im missing something but why cant you just use a while loop? this will give you the same loop forever if you have an exception (which is bad code) functionality that your code gives.

IDatabase database = null;

while(database == null){
   try
   {
        database = databaseLoader.LoadDatabase();
   }
   catch(Exception e)
   {
        var connector = _userInteractor.GetDatabaseConnector();
        if(connector == null)
                throw new ConfigException("Could not load the database specified in your config file.");
        databaseLoader = DatabaseLoaderFacade.GetDatabaseLoader(connector);
        //just in case??
        database = null;
   }
 }

if you have to use goto in your normal code, you're missing logical flow. which you can get using standard constructs, if, while, for etc..

Aran Mulholland
I should not have upvoted, this can loop forever.
MPelletier
It won't, because it is not loading the same database forever.
Jamie Penney
If LoadDatabase() returns null and does not fail, it will, no?
MPelletier
That is correct. I can't believe I missed that.
Jamie Penney
That method doesn't return null, but that knowledge shouldn't be used here.
Jamie Penney
if you look at the comment above the code you will note that i say that the code will go forever, as will the original code in the question, the answer is to do with the use of goto vs a while loop, not whether it is good code ( which i have stated it is not )
Aran Mulholland
even though this code is as bad as using a goto, it gets a little better if you used a while true loop and break after database = databaseLoader.LoadDatabase();
Johannes Rudolph
at the risk of starting an argument, break is just a form of goto as well. in normal code you never need to use it. (except in a case statement)
Aran Mulholland
+2  A: 

Is it clear? Not really. What you actually want to do, I think, is first try to load the database and then, if that didn't work, try to load it a different way. Is that right? Let's write the code that way.

IDatabase loadedDatabase = null;

// first try
try
{
    loadedDatabase = databaseLoader.LoadDatabase();
}
catch(Exception e) { }  // THIS IS BAD DON'T DO THIS

// second try
if(loadedDatabase == null) 
{
    var connector = _userInteractor.GetDatabaseConnector();
    if(connector == null)
        throw new ConfigException("Could not load the database specified in your config file.");
    databaseLoader = DatabaseLoaderFacade.GetDatabaseLoader(connector);
    loadedDatabase = databaseLoader.LoadDatabase()
}

This more clearly illustrates what you're actually doing. As an added bonus, other programmers won't gouge out your eyes. :)

NOTE: you almost certainly don't want to catch Exception. There's likely a more specific exception that you would rather be catching. This would also catch TheComputerIsOnFireException, after which it isn't really worth retrying.

Russell Mull
Ah, I see now that you want to keep asking the user for a different database connection each time it fails, so this won't work. But it should answer your question better: no, your construct with the goto isn't clear. :) A while loop as more appropriate, as others have noted.
Russell Mull
A: 

On a side note, I think there is potential for an endless loop if you always get an exception.

Technically there is nothing wrong with your goto structure, but for me, I would opt for using a while loop instead. Something like:

IDatabase database = null;

bool bSuccess = false;
int iTries = 0
while (!bSuccess) // or while (database == null)
{
    try
    {
        iTries++;
        database = databaseLoader.LoadDatabase();
        bSuccess = true;
    }
    catch(DatabaseLoaderException e)
    {
        //Avoid an endless loop
        if (iTries > 10)
             throw e;

        var connector = _userInteractor.GetDatabaseConnector();
        if(connector == null)
             throw new ConfigException("Could not load the database specified in your config file.");
        databaseLoader = DatabaseLoaderFacade.GetDatabaseLoader(connector);
    }
}
Jeremy
A: 
thecoop