I often write sequences of statements that are responsible for some sort of "cleanup", and which I would like to all run, regardless of whether the others fail. For example, I may have a class that adds a few elements to the DOM, and during cleanup, I want each of these elements removed. If I naively wrote
someParentNode.removeChild(someFirstElement);
someParentNode.removeChild(someSecondElement);
someParentNode.removeChild(someThirdElement);
and one of the first two statements completed abruptly, the third statement would not be executed. So to ensure that as much of my cleanup logic runs as possible, I instead write
try {
someParentNode.removeChild(someFirstElement);
} finally {
try {
someParentNode.removeChild(someSecondElement);
} finally {
someParentNode.removeChild(someThirdElement);
}
}
As you can see, this nesting gets messy and out of hand quickly. We could have sugar for such a pattern that would allow us to write this as a normal looking sequence of statements. As an example,
try all {
someParentNode.removeChild(someFirstElement);
someParentNode.removeChild(someSecondElement);
someParentNode.removeChild(someThirdElement);
}
Each statement (and the continuation) in the block would be in its own nested try-finally. Is this a pattern that other developers also frequently run into? Is it worth putting together a proposal for something like this? I imagine anyone who writes maximally defensive code like myself would do this.
I've never run into this issue; however, I can explain how I'd tackle your mentioned example.
Essentially, generally, if your code can throw, there's an unknown. In this case, it's whether or not a parent contains a child node. You can fix this by removing that uncertainty:
const removeIfChild = (parent, child) => {
if(child.parentNode == parent) parent.removeChild(child);
}
removeIfChild(someParentNode, someFirstElement);
removeIfChild(someParentNode, someSecondElement);
removeIfChild(someParentNode, someThirdElement);
In this case, it could be that you just want to remove the elements from the DOM, in which case you can simply do
someFirstElement.remove();
someSecondElement.remove();
someThirdElement.remove();
The point I'm trying to make is basically that to solve this issue, you can make sure your code does what it should do and remove these uncertainties by specifically checking for things that can happen so your code doesn't throw.
2 Likes
You might be interested if the Try-catch oneliner thread.
For your example it would look something like:
try someParentNode.removeChild(someFirstElement);
try someParentNode.removeChild(someSecondElement);
try someParentNode.removeChild(someThirdElement);
I like @vrugtehagel's suggestion - I always try to code in a way that makes anticipated failures explicit. It documents what kinds of failures you're expecting (making refactoring easier), and allows unexpected failures (like misspelling the variable name someSecondElement) to still throw a loud fuss, indicating there's a bug in the code.
If you do get too many nested try-finally, you can do something like the following snippet (if you're ok adding some abstraction to it). This could be extracted to a helper function too, if it's commonly used.
const errors = [
() => someParentNode.removeChild(someFirstElement),
() => someParentNode.removeChild(someSecondElement),
() => someParentNode.removeChild(someThirdElement),
].flatMap(fn => {
try {
fn()
} catch (err) {
return err
}
})
if (errors.length) throw errors[0] // or however you want to handle this
Though I admit, extra abstraction like that isn't the prettiest.
btw @theScottyJam flatMap
won't flatten undefined
. So I think you meant this
const errors = [
...
].flatMap(fn => {
try {
fn();
return [];
} catch (err) {
return [err];
}
})
:)
1 Like
Unfortunately, you don't always have some test you can perform to ensure the function you call won't throw. It's fine for simple DOM operations like in the example, but if it's just some black box function, I have to use a try
.
Well, there are two types of errors you might be dealing with: fatal programmer errors (like an invalid parameter) and handleable exceptions (like FileNotFound).
What kind of errors are you expecting to be thrown in your finally block, that makes you want to do a multi-tier finally? If they're handleable exceptions, then you can usually just catch and handle them right there. Or, are you trying to handle the case where each individual line of your "try all" could contain programmer errors - I guess it's possible there are use cases for that kind of error handling, but that seems more rare than common.
I've never needed a nested try-finally - most of the time when I'm using a finally, it's because I'm calling some passed-in function in the try block, which means I can't be certain it'll succeed or not, so I have to be careful to clean up allocated resources afterward. But, within a finally block, I'm usually not calling foreign code, so if I want to make sure it works, I can usually just test it.