19.7. ErrorsAn error is a message at runtime saying, in effect, that something bad has happened and execution cannot continue. The sender of such a message is said to throw an error. The message percolates up through the chain of handler calls (the call chain ), looking for an error-handling block surrounding the line currently being executed; such a block is said to catch the error. If no such block catches the error, it percolates all the way up to AppleScript, and the script terminates prematurely (possibly with an error dialog ). This entire mechanism is extremely nice, because it provides a target application, or AppleScript itself, with a way to signal that it's impossible to proceed, interrupting the flow of code while leaving it up to the caller whether and how to recover. Your script can implement no error handling, in which case any runtime error will bring the script to a grinding halt. Or your script can implement error handling in certain areas where it expects an error might occur. It can recover from some errors and re-throw others, allowing them to terminate the script. It can even throw an error deliberately as a way of controlling the flow of code. An error can be a positive thing, and can be built into the structure of a command's implementation. For example, display dialog throws an error if the user clicks the Cancel button in the dialog. This need not kill your script. It will if you let it, and this can be a good thing (because Cancel often means "stop"). But alternatively, your script can catch the error as a way of learning that the user has cancelled, and can then proceed in some other appropriate manner. I'll talk first about how to throw an error, then about how to catch one. 19.7.1. Throwing an ErrorTo throw an error, use the error command. It has five optional parameters: error [messageString] [number shortInteger] [partial result list] [from anything] [to class] Here are the default values of the parameters:
You can use any of the parameters when throwing an error, but in real life you are likely to use only the first two. The others are present because this is also the structure of an error message from an application, which can supply this further information to help explain what the problem was. If you throw an uncaught error, it will percolate all the way up to AppleScript and will be presented to the user as a dialog . The messageString is your chance to dictate what appears in that dialog. You will probably want to say something meaningful about what went wrong. For example: error "Things fall apart, the centre cannot hold." Figure 19-1 shows how that error is presented to the user in the Script Editor. If an error is thrown in an applet, the applet puts up a similar dialog, which also offers a chance to edit the script. If this is a Stay Open applet ("Applet Options" in Chapter 27), the error does not cause it to quit. If you don't supply any parameters at all to your error command, the error dialog reads: "An error has occurred." If you don't supply a messageString but you do supply an error numberlet's say it's 32the dialog reads: "An error of type 32 has occurred." Figure 19-1. An error dialogAn error number is not highly communicative to the user, unless the user happens to have a table of error numbers and their meanings, but it is certainly useful within code, particularly when you're implementing internal error handling. If different kinds of things can go wrong, you can use this number to signal which one did go wrong. An example appears in the next section. Error number -128 is special. If an error with this number percolates up to AppleScript, the script stops but no error dialog is displayed. This is the error number generated when the user presses Cancel in a dialog presented by an interface scripting addition command such as display dialog. Thus, the user can cancel without automatically being presented with an error message immediately after. ("User canceled." "I know that!") Some milieus never present an error message, regardless of the error. For example, if a script being run by Apple's Script Menu generates a runtime error, the script will simply fail silently. I regard this as a bug. 19.7.2. Catching an ErrorThe only way to catch an error is for that error to be thrown within a try block . The thrown error percolates up through the call chain , and if it eventually finds itself within a try block, it may be caught. The point of the call chain here is that the error need not occur directly within a try block; it may occur within in a handler that was called within a try block, or the call to that handler may be within a try block, and so forth. There are two forms of try block. In the first, there is no actual error-handling code: try -- code in which errors will be caught end try This form of try block handles the error by ignoring it. If an error occurs within the try block, the block terminates; execution resumes after the end try, and that's the end of the matter. Thus, you have no way to learn directly that an error was caught (though you can learn indirectly, because some code may have been skipped). But at least the error didn't bring your script to a halt. Here's an example: set x to "Cancel" try set x to button returned of (display dialog "Press a button.") end try display dialog "You pressed " & x Without the try block, this code would never reach the last line after the user presses the Cancel button. In this next example, we use a try block as a form of flow control, to terminate a loop prematurely (see "Looping," earlier in this chapter). We want to get the name of every disk. (Ignore the fact that we could just ask the Finder for this information directly.) Instead of asking how many disks there are and looping that number of times, we loop inside a try block. The loop is ostensibly endless, but in actual fact, when we exceed the number of disks, the Finder throws an error and the loop ends.
set L to {}
set x to 1
tell application "Finder"
try
repeat
set end of L to name of disk x
set x to x + 1
end repeat
end try
end tell
L -- {"feathers", "gromit", "Network"} In the second, fuller form of try block, you supply a second block, an error block , presumably containing some error-handling functionality: try -- code in which errors will be caught on error [parameters] -- error-handling code end try If an error is occurs within the try block (the part before the error block), the try block terminates; execution resumes at the start of the error block. If no error occurs in the try block, the error block is skipped. If an error occurs within the error block, it is not caught by this try block, because we are past that already; but it might be caught by some other try block that we are nested inside, either directly or further up the calling chain. Indeed, it is perfectly legitimate, and possibly useful, to throw an error within an error block. The parameters of an error block are exactly the same as those for an error command, so your error block can capture and respond to any information that may have been included when the error was thrown. You don't have to include any parameters and you can include any subset of the parameters; thus you aren't forced to capture information you don't care about. Parameter variable names are local to the error block . A not-uncommon technique is to include all the parameters and rethrow the very same error, or a slightly modified version of it, from within the error block. This could be a way, for instance, to shut things down in good order before letting the error percolate all the way to AppleScript and display a message. It can also be a way to tell yourself more about where the error occurred: on num(what) try return what as number on error s number i partial result p from f to t set s to "Handler num got an error: " & s error s number i partial result p from f to t end try end num num("howdy") -- error: Handler num got an error: Can't make "howdy" into type number Error handling and error throwing can be the basis of useful flow control. You can do some powerful things with errors that can't easily be accomplished in any other way. You can also structure your scripts better through the use of errors. In this example, flow control is implemented entirely through handler calls and errors. We ask the user for a number; if the user tries to cancel, or supplies something that can't be coerced to a number, AppleScript throws an error, and we start over recursively. The error thrown at the end of the askUser handler is a trick for returning the user's number directly without unwinding the entire recursion: on askUser( ) try set x to text returned of (display dialog "Give me a number:" default answer "") set x to (x as number) on error askUser( ) end try error x end askUser try askUser( ) on error what display dialog "Your number is " & what end try This next example is somewhat similar: it asks the user to enter the name of a color, and persists until the user complies. (In this example, unlike the previous one, if the user cancels, the script politely stops.) The example demonstrates how errors and error handling can help you organize the structure of a script: on getFavoriteColor( ) try set r to display dialog "What is your favorite color?" default answer "" on error error number 1001 end try set s to text returned of r if s = "" then error number 1000 return s end getFavoriteColor set c to "" repeat until c is not "" try set c to getFavoriteColor( ) on error number n if n = 1000 then display dialog "You didn't enter a color!" buttons "OK" else if n = 1001 then display dialog "Why did you cancel? Tell me!" buttons "OK" end if end try end repeat display dialog "Aha, you like " & c & ", eh?" In that example, the handler getFavoriteColor has just one jobto try to get the user's favorite color and report what happened. Either it returns the user's favorite color, or it throws error 1000 to signal that the user left the field blank in the dialog, or it throws error 1001 to signal that the user cancelled. It's up to the caller to decide how to proceed based on on this report. This particular caller has a different dialog ready to show the user in case of either error, and is perfectly prepared to loop all day until the user enters something in the dialog. But all of that is the caller's own decision; the handler itself just performs the single task for which it was written. Distribution of responsibilities makes for more reusable code, and the example shows how throwing errors contributes to this. A common technique in an error handler is to handle only those errors that are in some sense yoursthose that you expect and are prepared to deal with. Unexpected errors are simply allowed to percolate on up the call chain, possibly all the way to AppleScript, causing the script to terminate; this makes sense because they're unexpected and you're not prepared to deal with them. There are two ways to accomplish this. One way is to catch all errors and then rethrow any errors you aren't prepared to handle. If you're going to do that, you should probably use all the parameters, both in the on error line as you catch the error and in the error command as you rethrow it; otherwise you might strip the error of some of its information, which might reduce its value to the user (or to any code at some higher level that catches it). In this example, we ask the user for the number of a disk to get the name of. If the number is not the number of an existing disk, the Finder throws error number -1728, so if we get an error and that's its number, we deliver a meaningful response. If we get any other errorfor example, the user enters text in the dialog that can't be coerced to a numberwe rethrow it and let AppleScript inform the user that this is not an integer. set n to text returned of (display dialog "What disk would you like the name of?" default answer "") try tell application "Finder" to set x to name of disk (n as integer) display dialog x on error e number n partial result p from f to t if n = -1728 then display dialog "I don't think that disk exists. " & e else error e number n partial result p from f to t end if end try The other approach is to use a filtered error handler. In this approach, some of the parameters in the on error line are not variable names but literals. AppleScript will call the error block only if all such literals are matched by the corresponding error parameter value. Otherwise, the error percolates up the call chain, of its own accord. Thus, we can rewrite the entire error block from the previous example in a much briefer form, as follows: on error e number -1728 display dialog "I don't think that disk exists. " & e end try There's no way to list alternative literals; you can't write an error block that catches errors with either of just two particular error numbers, for instance. A workaround is to nest try blocks. To illustrate, here's the same example again, but this time we'll catch both error -1728 (no such disk) and error -1700 (not an integer). set n to text returned of (display dialog "What disk would you like the name of?" default answer "") try try tell application "Finder" to set x to name of disk (n as integer) display dialog x on error e number -1728 display dialog "I don't think that disk exists. " & e end try on error e number -1700 display dialog "I don't think that was an integer." end try If you don't like the look of literally nested try blocks ("lexical nesting "), you can nest them by means of the calling chain ("dynamic nesting "): on askUser( ) set n to text returned of (display dialog "What disk would you like the name of?" default answer "") try tell application "Finder" to set x to name of disk (n as integer) display dialog x on error e number -1728 display dialog "I don't think that disk exists. " & e end try end askUser try askUser( ) on error e number -1700 display dialog "I don't think that was an integer." end try An expired timeout (see "Timeout," earlier in this chapter) is an error like any other; this example shows a way to handle it: try tell application "Finder" activate with timeout of 1 second display dialog "Press a button." giving up after 2 end timeout end tell on error number -1712 activate display dialog "Ha ha, not fast enough!" end try |