diff --git a/src/core/friendly_errors/fes_core.js b/src/core/friendly_errors/fes_core.js index 8962745918..72f5618d97 100644 --- a/src/core/friendly_errors/fes_core.js +++ b/src/core/friendly_errors/fes_core.js @@ -724,7 +724,10 @@ function fesCore(p5, fn){ stacktrace && stacktrace[0].fileName && stacktrace[0].lineNumber && - stacktrace[0].columnNumber + stacktrace[0].columnNumber && + friendlyStack && + friendlyStack[0] && + friendlyStack[0].lineNumber ) { locationObj = { location: `${stacktrace[0].fileName}:${stacktrace[0].lineNumber}:${ @@ -1036,7 +1039,17 @@ function fesCore(p5, fn){ }) .map(name => { let type; - + + // Use getOwnPropertyDescriptor to avoid triggering getters + // that depend on instance state (like 'pixels' which accesses this._renderer) + const descriptor = Object.getOwnPropertyDescriptor(obj, name); + + if (descriptor && descriptor.get) { + // It's a getter, skip it to avoid accessing instance properties + // when obj is the prototype + return null; + } + if (typeof obj[name] === 'function') { type = 'function'; } else if (name === name.toUpperCase()) { @@ -1046,7 +1059,8 @@ function fesCore(p5, fn){ } return { name, type }; - }); + }) + .filter(symbol => symbol !== null); misusedAtTopLevelCode = [].concat( getSymbols(fn), diff --git a/test/unit/core/fes_error_monitoring.js b/test/unit/core/fes_error_monitoring.js new file mode 100644 index 0000000000..b9f4d35477 --- /dev/null +++ b/test/unit/core/fes_error_monitoring.js @@ -0,0 +1,111 @@ +import p5 from '../../../src/app.js'; + +suite('FES Error Monitoring', function () { + let myp5; + let logs = []; + let originalFesLogger; + + beforeEach(function () { + logs = []; + myp5 = new p5(function () {}); + // Use _fesLogger to capture FES error messages (proper way to test FES) + originalFesLogger = myp5._fesLogger; + myp5._fesLogger = function (...args) { + logs.push(args.join(' ')); + }; + }); + + afterEach(function () { + myp5._fesLogger = originalFesLogger; + if (myp5) { + myp5.remove(); + } + }); + + test('should handle ReferenceError without throwing when friendlyStack is null', function (done) { + // This test verifies the fix for issue #8381: FES should not throw an error + // when friendlyStack is null (which happens when processStack returns [false, null]) + // Before the fix, accessing friendlyStack[0].lineNumber would throw a TypeError, + // causing a second error that masks the actual ReferenceError + // + // This test reproduces the scenario from the example sketch: + // https://editor.p5js.org/davepagurek/sketches/aVaqe0sNn + // where accessing an undefined variable (like `s + 20`) triggers a ReferenceError + // that goes through handleMisspelling -> get pixels -> fesErrorMonitor + + // Track if FES itself throws an error (the bug we're fixing) + let fesThrewError = false; + let referenceErrorCaught = false; + let errorMessages = []; + const originalErrorHandler = window.onerror; + + // Set up error handler to catch any errors + window.onerror = function(message, source, lineno, colno, error) { + errorMessages.push({ + message: message, + name: error ? error.name : 'Unknown', + errorMessage: error ? error.message : message + }); + + // Check if this is the specific TypeError from FES accessing friendlyStack[0].lineNumber + // This is the bug we're fixing: when friendlyStack is null, accessing friendlyStack[0].lineNumber + // at line 730 in fes_core.js throws "Cannot read property 'lineNumber' of undefined" + // We specifically check for errors about 'lineNumber' to avoid false positives from + // other TypeErrors (like the 'pixels' error from handleMisspelling) + if (error && error.name === 'TypeError' && error.message && + (error.message.includes('lineNumber') || + error.message.includes("can't access property 'lineNumber'"))) { + fesThrewError = true; + } + // Check if this is the original ReferenceError + if (error && error.name === 'ReferenceError') { + referenceErrorCaught = true; + } + return false; // Don't prevent default error handling + }; + + // Create a p5 instance that will trigger a ReferenceError naturally + // Similar to the example sketch: console.log(s + 20) where s is undefined + // This will trigger the same code path: ReferenceError -> handleMisspelling -> fesErrorMonitor + const testP5 = new p5(function(p) { + p.setup = function() { + p.createCanvas(100, 100); + }; + + p.draw = function() { + // Access undefined variable to trigger ReferenceError naturally + // This mimics the example sketch: console.log(s + 20) + // The error will propagate to the browser's error handler, which FES monitors + // This triggers the same code path as the example: handleMisspelling -> fesErrorMonitor + const result = s + 20; // eslint-disable-line no-undef + p.noLoop(); // Stop after first frame + }; + }); + + // Give FES time to process the error + setTimeout(function() { + window.onerror = originalErrorHandler; + + // FES should not throw its own error + // This is the core fix: before the fix, accessing friendlyStack[0].lineNumber + // when friendlyStack is null would throw a TypeError, causing a second error + assert.isFalse( + fesThrewError, + 'FES should not throw its own error when handling ReferenceError. ' + + 'If this fails, FES is throwing a TypeError when friendlyStack is null. ' + + 'Error messages: ' + JSON.stringify(errorMessages, null, 2) + ); + + // Should have caught the original ReferenceError + assert.isTrue( + referenceErrorCaught, + 'Should have caught the original ReferenceError from undefined variable. ' + + 'Error messages: ' + JSON.stringify(errorMessages, null, 2) + ); + + testP5.remove(); + done(); + }, 200); + }); +}); +