Friday Puzzle: What does a Catch block catch?

On Friday, January 6, 2017, I posted the following puzzle on Twitter and Facebook (here, too!).

It shows a Try block that encloses an expression with an intentional error and a Catch block that writes an error. But, when you run it, the error doesn’t appear as designed.

The error is supposed to say ‘Cannot multiply by apple.’ But, it doesn’t. What went wrong here?

I marked this week’s puzzle “beginner” not because it’s easy or obvious, but because almost everyone who’s written error handling for a PowerShell script has already run into this and is familiar with it.

The Solution: Catch catches the error

The answer is that the Catch block catches terminating errors. In a Catch block, $_ represents the error or exception that it caught. It does not contain any of the objects in the (now terminated) pipeline.

Let’s run the code and see what happens.

An error occurs when PowerShell tries to multiply a string (‘car’) by another string (‘apple’). It’s a terminating error, that is, a serious error that stops the pipeline, so it falls into the Catch block, as designed. So far, so good.

But, the error that the Catch block generates does not say ‘Cannot multiply by apple’. Instead, it says (color added for emphasis):

Cannot multiply by Cannot convert value "apple" to type "System.Int32". Error: "Input string was not in a correct format.".

The Cannot multiply by comes from the Write-Error message string. The next part of the message string is $_, the current object in the pipeline. But, the value of $_ is not ‘apple’. Instead, its value is:

Cannot convert value "apple" to type "System.Int32". Error: "Input string was not in a correct format.".

And, the type of the current object is not a string, like ‘apple’. It’s an ErrorRecord. To figure this out, I comment-out the Write-Error command and replace it with strings that write the value of the current object and its type.

These diagnostic strings — the ones that write out values and types — are one of my favorite tools. I use them whenever I get unexpected output.

What happened here is now a bit clearer.

We started with an array of objects:

1, 2, 'apple', 3

And piped them to the ForEach-Object cmdlet.

1, 2, 'apple', 3 | ForEach-Object { <expression> }

ForEach-Object gets the items in the array one at a time and includes them in the expression. So, in the ForEach-Object expression, the value of $_ is the current array item.

PS C:\> 1, 2, 'apple', 3 | ForEach-Object { "The current object: $_" }
The current object: 1
The current object: 2
The current object: apple
The current object: 3

The expression multiples a string (‘car’) by the current object. When the current object is an integer, the multiplication works.

PS C:\> 'car' * 3
carcarcar

NOTE: In PowerShell, multiplication is not commutative: (‘car’ * 3) -ne (3 * ‘car’). For more information, see about_arithmetic_operators.

But, PowerShell won’t let you multiply a string by another string.

PS C:\> 'car' * 'apple'
Cannot convert value "apple" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:1
+ 'car' * 'apple'
+ ~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastFromStringToInteger

When you try to multiply two strings, PowerShell declares a terminating error, an error so serious that it terminates or stops the pipeline. If the terminating error occurs in a Try block, as in this case, control falls to the Catch block. The $_ still works, but the current object ($_) in the Catch block is not an item in the array of the now-terminated pipeline. It is the terminating error or exception that stopped the pipeline in the Try block.

In this case, the terminating error, the value of $_, is:

Cannot convert value "apple" to type "System.Int32". Error: "Input string was not in a correct format.".

The Fix

Once you’ve figured out what went wrong, it’s easy to come up with several different solutions. The simplest solution is to add an expression to the ForEach-Object script block that assigns the current object to a variable (in this case, $x). Each time an object in the pipeline goes through the ForEach-Object script block, its value is assigned to $x, replacing the previous value. Then, if a terminating error occurs, you can use the $x variable in the error message.

When I first tried this solution many years ago, I was worried that the Try and Catch script blocks might have different scopes, requiring us to define the variable outside of the Try-Catch, but fortunately, they’re in the same scope. And terminating the pipeline doesn’t delete variables created in the pipeline.

Someone suggested using the PipelineVariable common parameter to create the variable that saves the current object, but the pipeline variable is valid only as long as the pipeline is active.

You might notice that, in the fix, I replaced the Write-Error cmdlet with the Throw statement. Write-Error works, too, but I usually omit it from Catch blocks because some folks think that it converts a terminating error to a non-terminating error, allowing the pipeline to continue. It doesn’t and I don’t want to confuse people, so, in my code, I use Throw.

Summary

The moral of the story is to test everything in your scripts, including the errors. If the errors don’t print as you expected, throw some debugging code in there.

Thanks to everyone on Facebook and Twitter who tried the puzzle. Thanks to those who submitted guesses (they were terrific!) and those who refrained to give beginners a chance.

For more information about error handling in PowerShell, including the difference between terminating and non-terminating errors, see Dave Wyatt‘s awesome: The Big Book of PowerShell Error Handling. For help with the Try-Catch block, see about_Try_Catch_Finally. For help with PowerShell arithmetic, see about_arithmetic_operators.

And, for some truly fun stuff with PowerShell arithmetic, see Fun Formatting Ones, Part 1: The Task and Fun Formatting Ones, Part 2: The Method, where we examine some really clever code by Doug Finke.

Like this Friday PowerShell Puzzle? Find more puzzles here. If you have a PowerShell puzzle suggestion, post it here or ping me on Twitter at @juneb_get_help.

June Blender is a technology evangelist at SAPIEN Technologies, Inc and a Microsoft Cloud & Datacenter MVP. You can reach her at juneb@sapien.com or follow her on Twitter at @juneb_get_help.