Getting the stack trace of an error in ExtendScript
Asked Answered
L

5

9

When I catch an error in ExtendScript, I would like to be able to log its stack trace. It appears that errors do not contain stack traces in ExtendScript, so I'm playing around with the idea of adding stack traces to errors.

The only way I know of to get a stack trace is $.stack. The field $.stack contains the current stack trace at the moment that you access the field.

My first attempt was to create my own error object that includes the stack. The Error object is very special in that it can get the line and filename of the code that created it. For example,

try {
    throw new Error("Houston, we have a problem.");
}
catch (e) {
    $.writeln("Line: " + e.line);
    $.writeln("File: " + e.fileName);
    $.writeln("Message: " + e.message);
}

Will print:

Line: 2
File: ~/Desktop/Source1.jsx
Message: Houston, we have a problem.

I don't think it's possible to create your own object with this ability. The closest I can get is this:

function MyError(msg, file, line) {
    this.message = msg;
    this.fileName = file;
    this.line = line;
    this.stack = $.stack;
}

try {
    throw new MyError("Houston, we have a problem.", $.fileName, $.line);
}
catch (e) {
    $.writeln("Line: " + e.line);
    $.writeln("File: " + e.fileName);
    $.writeln("Message: " + e.message);
    $.writeln("Stack: " + e.stack);
}

Which prints:

Line: 9
File: ~/Desktop/Source2.jsx
Message: Houston, we have a problem.
Stack: [Source3.jsx]
MyError("Houston, we have a p"...,"~/Desktop/Source2.js"...,9)

Here we can see that I'm creating my own error object and explicitly passing it the line and file name (since MyError can't figure that out on its own). I've also included the current stack when the error gets created.

This works fine when I call my own error object, but it doesn't work when other code calls the regular Error object or when an error is generated automatically (e.g. by illegal access). I want to be able to get the stack trace of any error, no matter how it is generated.

Other approaches might be to modify Error's constructor, modify Error's prototype, or replace the Error object entirely. I haven't been able to get any of these approaches to work.

Another idea would be to put a catch block in every single method of my code and add the current stack to the error if it doesn't already have one. I would like to avoid this option if possible.

I'm out of ideas. Is there any way to get the stack trace of errors?

Lowminded answered 24/4, 2013 at 20:29 Comment(2)
I'd be really interested if you get something working. My biggest problem is as soon as I compile to a jsxbin format I get nothing useful from the errors. I've gone the route of an incredible amount of logging happening so I can debug from that.Kucera
Personnaly I woudl'nt use $.write or $.writeln instructions. First of all they will bring ExtendScript Toolkit upfront if not opened yer. That can be really confusing end-users if not they arn't warned. Also it can be really really slow especially inside loops. I prefer writing log files to disk. I have my own recipe but you can take advantage of this lib:creative-scripts.com/logging-with-a-smileEucharist
L
2

It isn't perfect, but I found a partial solution.

Fact 1: Error.prototype is an Error object.

Fact 2: The method Error.prototype.toString is called whenever an error is created.

Fact 3: The field Error.prototype.toString can be modified.

That method typically just returns the string "Error", so we can replace it with our own method that stores the stack and then returns the string "Error".

Error.prototype.toString = function() {
    if (typeof this.stack === "undefined" || this.stack === null) {
        this.stack = "placeholder";
        // The previous line is needed because the next line may indirectly call this method.
        this.stack = $.stack;
    }
    return "Error";
}

try {
    throw new Error("Houston, we have a problem.");
}
catch (e) {
    $.writeln("Line: " + e.line);
    $.writeln("File: " + e.fileName);
    $.writeln("Message: " + e.message);
    $.writeln("Stack: " + e.stack);
}

Result:

Line: 11
File: ~/Desktop/Source10.jsx
Message: Houston, we have a problem.
Stack: [Source10.jsx]
toString()

It works! The only problem is automatic errors.

Error.prototype.toString = function() {
    if (typeof this.stack === "undefined" || this.stack === null) {
        this.stack = "placeholder";
        // The previous line is needed because the next line may indirectly call this method.
        this.stack = $.stack;
    }
    return "Error";
}

try {
    var foo = null;
    foo.bar;
}
catch (e) {
    $.writeln("Line: " + e.line);
    $.writeln("File: " + e.fileName);
    $.writeln("Message: " + e.message);
    $.writeln("Stack: " + e.stack);
}

Result:

Line: 12
File: ~/Desktop/Source12.jsx
Message: null is not an object
Stack: undefined

So it doesn't work on all errors, but its progress.

Lowminded answered 25/4, 2013 at 16:16 Comment(0)
L
2

I've come up with another solution, though this one requires you to change some of your code. Instead of calling methods as usual:

myObject.myMethod1("Hello", "world");

You'll need to switch to calling methods like this:

myObject.do("myMethod1", "Hello", "world");

Here's a complete example of how it works:

Object.prototype.do = function stackHelper() {
    // Convert the arguments into an array.
    var argumentArray = Array.prototype.slice.call(arguments);
    // Remove the first argument, which is the function's name.
    var functionString = argumentArray.shift();
    try {
        this[functionString].apply(this, argumentArray);
    }
    catch (e) {
        if (typeof e.stack === "undefined" || e.stack === null) {
            e.stack = $.stack;
        }
        throw e;
    }
};

var myObject = {
    myMethod1: function myMethod1(myArg1, myArg2){
        this.do("myMethod2", myArg1, myArg2);
    },

    myMethod2: function myMethod2(myArg1, myArg2){
        this.do("myMethod3", myArg1, myArg2);
    },

    myMethod3: function myMethod3(myArg1, myArg2){
        $.writeln(myArg1 + ", " + myArg2 + "!");
        var foo = null;
        foo.bar; // Throws an error.
    },
};

try {
    myObject.do("myMethod1", "Hello", "world");
}
catch (e) {
    $.writeln("Stack: " + e.stack);
}

The output looks like this:

Hello, world!
Stack: [do.jsx]
stackHelper("myMethod1","Hello","world")
myMethod1("Hello","world")
stackHelper("myMethod2","Hello","world")
myMethod2("Hello","world")
stackHelper("myMethod3","Hello","world")

It's not a great solution, but at least it works on all errors.

Lowminded answered 10/5, 2013 at 16:54 Comment(0)
C
1

As far as I know you cannot modify the [native Code] of the Error.prototype.toString-function. So I came up with this solution:

function ReturnCustomErrorString(e, additionalMessage)
{
    try {
        var errorString = e.toString();
        errorString = errorString.concat("\n", "additionalMessage: " + additionalMessage + "\n", "file: " + e.fileName + "\n", "line: " + e.line + "\n", "stack-trace: \n" + $.stack);
        return errorString;
    }
    catch (e) {
        alert("Error in : " + ReturnCustomErrorString.name + "(...)\n" + e);
        exit();
    }
}

Usage:

try {
    // code that does throw an error
} catch (e) { 
    alert(ReturnCustomErrorString(e)); 
}

Before I wrote this function I often did something like this in the catch-block:

alert(e);

Now I'm doing alert(ReturnCustomErrorString(e));, but I get much more useful information. So at the moment I think this solution is pretty good.

Camala answered 26/7, 2017 at 21:11 Comment(0)
R
0

If you need simply show a custom message, I wrote this code. I think that solved, ... for me it's ok.

try
{
    app.selection[0].contents = 1
}
catch (myError)
{
    alert(myError.number); // For check the number error
    if (myError.number == 30477)
    {
        alert("Mensagem Edu\n" + "Line: " + myError.line + "\n" + "File: " + myError.fileName + "\n" + "Message: " + myError.message + "\n" + "Stack: " + myError.stack);
        exit();
    }
    else (myError);
    {}
    exit();
}
Richie answered 28/5, 2016 at 17:53 Comment(1)
Does anybody get anything else than undefined for myError.stack, which is the point of the OP’s question?Maneating
T
0

I also found that extending the Error object causes issues, it has a "special status" in ExtendScript, unfortunately.

The best I could come up with is the following:

function WrappedError(error) {
    return {
        name: 'ExtendScriptError',
        message: error.message,
        source: error.source,
        line: error.line,
        stack: $.stack,
    }
}

Which is used like this:

throw WrappedError(new Error('Error Message Goes Here'))

The key to making it work is creating a real "Error" (object), on the actual line where the error is occurring, this way, we can get the correct line number in our wrapped error, and we also have access to the "err.source", which will be interesting for providing context later.

Next, when evaluating ExtendScript code from the "CEP" side, I wrap the call in a try / catch:

function safeRun(code) {
    const safeCode = `(function () {
        function errorToPretty (err) {
            var stack = (err.stack && err.stack.split('\\n')) || []
            var lines = (err.source && err.source.split('\\n')) || []
            err.line--
            stack.shift()
            stack.shift()
            stack.pop()
            stack.reverse()
            return {
                name: err.name,
                message: err.message,
                line: err.line,
                code: err.code,
                context: [
                    lines[err.line - 2] || '',
                    lines[err.line - 1] || '',
                    '---> ' + lines[err.line] || '',
                    lines[err.line + 1] || '',
                    lines[err.line + 2] || ''
                ],
                stack: stack
            }
        }
        try {
            return ${code}
        } catch (err) {
            return JSON.stringify(errorToPretty(err))
        }
    })()`
    return evalExtendscript(safeCode).then((res) => {
        if (typeof res === 'object' && res.stack && res.line && res.message && res.context) {
            const e = new Error(res.message + '\n\n' + res.context.join('\n'))
            e.name = res.name
            e.stack = `${res.name}: ${res.message}
        ${res.stack.map((func) => `at ${func} (extendscript.js:0:0)`).join('\n    ')}`
            throw e
        }
        return res
    })
}
Tuberculosis answered 7/9, 2021 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.