JXA Example of how to parse ObjC Error Objects

Here’s some JavaScript for Automation (JXA) code that shows how to use and parse the NSError object returned from many ObjC functions:

(function() {
    'use strict';

    // Short demo script to show how to use an "NSError" object that gets returned
    // from some ObjC functions.
    //
    // We use a function we're calling "getDirectoryContents" as the test method.
    // We'll call it once with a valid parameter, just to prove it works, then
    // call it with an invalid parameter, to see the results of the error.

    var contents;

    // Just to show us this function works, we'll run it against a know folder,
    // which will always have something in it:
    contents = getDirectoryContents("/Users");
    console.log("Count of files in existing folder: " + contents.length);

    // This should fail:
    contents = getDirectoryContents("Path to (hopefully) non existent folder");
    console.log("We shoudn't get here. " + contents.length);

    return;

// ----------------------------------------------------------------------------

    function getDirectoryContents(path) {
        // This is how you initialisze the error object. I'm calling the
        // variable "nsError" because that's the "type" of value returned. So
        // if you want to KMWindow what properties and methods are available,
        // look up "NSError".
        var nsError = $();

        // Call the desired function, making sure we unwrap the result.
        // If we don't unwrap the result, then we can't check "(!result)",
        // because result will always have a value.
        var result = ObjC.deepUnwrap(
                $.NSFileManager.defaultManager.contentsOfDirectoryAtPathError(
                    $(path).stringByStandardizingPath, nsError)
                );

        // According to the Apple docs, if the result of the function we
        // called (contentsOfDirectoryAtPath:Error) is nil, an error occurred
        // and the error information is returned in the "error" parameter.
        if (!result) {

            // ***** HERE'S HOW TO PARSE THE NSerror RESULT ********
            var errorMessage = $(nsError.localizedDescription).js;

            // But this is probably safer:
            errorMessage = getErrorMessage(nsError,
                "Could not get contents of folder '" + path + "'");

            throw Error(errorMessage);
        }

        return result;
    }

    // This is probably a safer way of getting the error message. If we've done
    // something wrong and nsError isn't valid, this returns the default error
    // message, or a generic message if the default isn't specified.
    function getErrorMessage(nsError, defaultErrorMessage) {
        try {
            return $(nsError.localizedDescription).js;
        } catch (e) {
            return defaultErrorMessage || "Unknown Error";
        }
    }
})();

A footnote to this useful page – if we delay the unwrapping, we can use the .isNil() method to test whether evaluation to an NSObject value has succeeded.

(See the Bridged Nil slides in #WWDC14 Session 306 'JavaScript for Automation')

Used here, with another approach (an option type, as in Swift, Haskell etc) to separating and making use of both the results channel (Right) and the error message channel (Left).

Either an error dialog is shown, or an array of file names is returned:

(() => {
    'use strict';

    const main = () => {

        // getDirectoryContentsLR :: FilePath -> Either String IO [FilePath]
        const getDirectoryContentsLR = strPath => {
            const
                error = $(),
                xs = $.NSFileManager.defaultManager
                .contentsOfDirectoryAtPathError(
                    $(strPath).stringByStandardizingPath,
                    error
                );
            return xs.isNil() ? (
                Left(ObjC.unwrap(error.localizedDescription))
            ) : Right(ObjC.deepUnwrap(xs))
        };

        return either(
            alert('getDirectoryContentsLR'),
            xs => {
                // List of file-names available for
                // some kind of use here.

                // e.g.
                return JSON.stringify(xs, null, 2)
            },
            getDirectoryContentsLR('~')
        );

    };

    // JXA ------------------------------------------------

    // alert :: String -> String -> IO String
    const alert = title => s => {
        const
            sa = Object.assign(Application('System Events'), {
                includeStandardAdditions: true
            });
        return (
            sa.activate(),
            sa.displayDialog(s, {
                withTitle: title,
                buttons: ['OK'],
                defaultButton: 'OK'
            }),
            s
        );
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = (fl, fr, e) =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // MAIN ---
    return main();
})();
2 Likes