It's okay. A refactoring to consider is pushing the code into its own method, and using early exits for success, letting you write the different attempts to do something at the same level:
try {
// do something
return;
} catch (Exception e) {
// fall through; you probably want to log this
}
try {
// do something in the same line, but being less ambitious
return;
} catch (Exception e) {
// fall through again; you probably want to log this too
}
try {
// Do the minimum acceptable
return;
} catch (Exception e) {
// if you don't have any more fallbacks, then throw an exception here
}
//More try catches?
Once you have it broken out like that, you could think about wrapping it up in a Strategy pattern.
interface DoSomethingStrategy {
public void doSomething() throws Exception;
}
class NormalStrategy implements DoSomethingStrategy {
public void doSomething() throws Exception {
// do something
}
}
class FirstFallbackStrategy implements DoSomethingStrategy {
public void doSomething() throws Exception {
// do something in the same line, but being less ambitious
}
}
class TrySeveralThingsStrategy implements DoSomethingStrategy {
private DoSomethingStrategy[] strategies = {new NormalStrategy(), new FirstFallbackStrategy()};
public void doSomething() throws Exception {
for (DoSomethingStrategy strategy: strategies) {
try {
strategy.doSomething();
return;
}
catch (Exception e) {
// log and continue
}
}
throw new Exception("all strategies failed");
}
}
Then just use the TrySeveralThingsStrategy, which is a kind of composite strategy (two patterns for the price of one!).
One huge caveat: don't do this unless your strategies are themselves sufficiently complex, or you want to be able to use them in flexible ways. Otherwise, you're larding a few line of simple code with a huge pile of unnecessary object-orientation.