Skip to content

Surface JavaScript file evaluation exceptions to native callers - #225

Closed
jeremytondo wants to merge 1 commit into
cmsj:mainfrom
jeremytondo:fix/evaluation-exceptions
Closed

jeremytondo wants to merge 1 commit into
cmsj:mainfrom
jeremytondo:fix/evaluation-exceptions

Conversation

@jeremytondo

Copy link
Copy Markdown

evalFromURL currently returns normally when JavaScript evaluation throws, so a native caller cannot distinguish a failed configuration from a successful one. Store the exception in the context's exception handler and throw a native error containing the source path, message, and stack. Clear the previous exception before each file evaluation.

The context remains alive: objects created before a runtime exception continue to work, while syntax errors execute nothing. This lets an embedding app present a recoverable configuration error without removing earlier working scripts. Regression tests cover partial execution, syntax errors, and successful evaluation after an error.

Validation: the patched engine builds at c0bd4d6 in Atelier's app target. Its isolated packaged lifecycle probe verifies partial-load recovery, syntax-error handling, relative modules/Spoons, retained independent timers, and 50 context reloads. The new upstream test cases have not been run in the unmodified upstream app host; the equivalent behavior was exercised through that packaged probe.

@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes file-based JavaScript evaluation failures visible to native callers by retaining the JavaScript exception and throwing an NSError containing its source path, message, and stack.

  • Clears previous exception state before each file evaluation.
  • Adds coverage for partial execution, syntax errors, context survival, and successful evaluation after an error.
  • Leaves the captured exception in the shared context after throwing, which can contaminate later exception polling.

Confidence Score: 4/5

The PR appears safe to merge after addressing the non-blocking stale exception-state issue.

Native exception propagation and recovery are covered, but a failed file evaluation leaves context.exception populated, allowing later unrelated exception checks to report the old configuration error.

Files Needing Attention: Hammerspoon 2/Engine/JSEngine.swift

Important Files Changed

Filename Overview
Hammerspoon 2/Engine/JSEngine.swift Adds native propagation of JavaScript file-evaluation exceptions, but does not clear the captured shared exception after constructing the native error.
Hammerspoon 2Tests/IntegrationTests/JSEngineEvaluationTests.swift Adds correctly included regression tests for runtime and syntax failures, partial execution, and subsequent recovery.
Prompt To Fix All With AI
### Issue 1
Hammerspoon 2/Engine/JSEngine.swift:163-168
**Exception state remains stale**

After converting the JavaScript exception into an `NSError`, this path throws without clearing `context.exception`. A later successful script that checks this shared slot, such as the docs module, can therefore report the previous configuration failure as its own. The stale exception can also escape a nested module evaluation whose Swift error was intentionally swallowed. Preserve the message and stack locally, then clear the slot before throwing.

```suggestion
        if let exception = context?.exception {
            let message = exception.toString() ?? "JavaScript error"
            let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
            context?.exception = nil
            throw NSError(domain: "Hammerspoon.Configuration", code: 1,
                          userInfo: [NSLocalizedDescriptionKey: "\(url.path): \(message)\n\(stack)"])
        }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Surface JavaScript file evaluation excep..." | Re-trigger Greptile

Comment on lines +163 to +168
if let exception = context?.exception {
let message = exception.toString() ?? "JavaScript error"
let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
throw NSError(domain: "Hammerspoon.Configuration", code: 1,
userInfo: [NSLocalizedDescriptionKey: "\(url.path): \(message)\n\(stack)"])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Exception state remains stale

After converting the JavaScript exception into an NSError, this path throws without clearing context.exception. A later successful script that checks this shared slot, such as the docs module, can therefore report the previous configuration failure as its own. The stale exception can also escape a nested module evaluation whose Swift error was intentionally swallowed. Preserve the message and stack locally, then clear the slot before throwing.

Suggested change
if let exception = context?.exception {
let message = exception.toString() ?? "JavaScript error"
let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
throw NSError(domain: "Hammerspoon.Configuration", code: 1,
userInfo: [NSLocalizedDescriptionKey: "\(url.path): \(message)\n\(stack)"])
}
if let exception = context?.exception {
let message = exception.toString() ?? "JavaScript error"
let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
context?.exception = nil
throw NSError(domain: "Hammerspoon.Configuration", code: 1,
userInfo: [NSLocalizedDescriptionKey: "\(url.path): \(message)\n\(stack)"])
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: Hammerspoon 2/Engine/JSEngine.swift
Line: 163-168

Comment:
**Exception state remains stale**

After converting the JavaScript exception into an `NSError`, this path throws without clearing `context.exception`. A later successful script that checks this shared slot, such as the docs module, can therefore report the previous configuration failure as its own. The stale exception can also escape a nested module evaluation whose Swift error was intentionally swallowed. Preserve the message and stack locally, then clear the slot before throwing.

```suggestion
        if let exception = context?.exception {
            let message = exception.toString() ?? "JavaScript error"
            let stack = exception.objectForKeyedSubscript("stack")?.toString() ?? ""
            context?.exception = nil
            throw NSError(domain: "Hammerspoon.Configuration", code: 1,
                          userInfo: [NSLocalizedDescriptionKey: "\(url.path): \(message)\n\(stack)"])
        }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6d8ae8. Exception capture is now scoped to file evaluation, the previous handler is restored, and the shared exception is cleared before throwing. The packaged probe now lets an independent timer run before any subsequent evaluation and verifies it keeps firing; the upstream regression tests also assert the context has no stale exception. The rebuilt packaged lifecycle probe passed, including 50 reloads.

@jeremytondo
jeremytondo deleted the fix/evaluation-exceptions branch September 14, 2026 14:54
@jeremytondo

Copy link
Copy Markdown
Author

Sorry about this PR. Was just messing around with something and a rouge agent decided it seemed like a good idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant