Exceptions: JavaScriptException.Location returns by value, so a net472 failure reports instead of crashing the host - #3564
Merged
lahma merged 1 commit intoSep 1, 2026
Conversation
…2 failure reports instead of crashing the host `JavaScriptException.Location` has been `ref readonly` since sebastienros#1270. Nothing calls it by name in a way that notices, but a public property of an exception is read *reflectively* by everything that renders a failed run - a test runner, a structured logger, an error page - and .NET Framework answers `PropertyInfo.GetValue` on a by-ref-returning property with `NotSupportedException: ByRef return value not supported in reflection invocation` rather than dereferencing it the way .NET Core does. Under NUnit that is not a degraded message, it is a lost run. `ExceptionHelper.AppendExceptionProperties` guards the getter with `catch (TargetInvocationException)`, and this exception is raised by `GetValue` itself before any invocation, so it escapes: the failure message names the reflection limitation instead of the JavaScript error, the worker thread dies, and the run ends with "The active test run was aborted. Reason: Test host process crashed" - every test still queued behind the red one gone with it. Reproduced on this tree before the change with four failing test cases: 2 of 5 reported, host crashed, and `dotnet test` never returned. After: 4 failed, 1 passed, 5 of 5, exit 1. `Location` therefore returns the value. Reading it is unchanged (`ex.Location.Start.Line`, `ex.Location != default`); only `ref readonly var l = ref ex.Location` stops compiling, which is `docs/v5-migration.md` 3.18. The private inner exception a renderer reaches through `InnerException` keeps its by-ref accessor and turns it `internal`, which is all it takes to hide it from `BindingFlags.Public`. `Jint.Tests.PublicInterface/ExceptionPropertyReflectionTests.cs` holds every `Exception` in the assembly to the rule structurally - so a future one fails on net8.0 and net10.0 too, not only on the leg that would crash - and replays what a renderer actually does over a real thrown `JavaScriptException`. The four remaining by-ref public properties are on non-exception types (`Completion`, `CallFrame`, `DebugInformation`, `ExceptionThrownEventArgs`) and stay as they are; `Completion.Location` in particular is genuinely hot. NUnit's half of it is reported upstream: a renderer should not be able to take the host down over a getter shape, whatever the library. Closes sebastienros#3549 Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3549.
On
net472, a test that fails by letting aJavaScriptExceptionescape does not report a failure — it takesthe test host down, and every test still queued in that process goes with it.
Repro, before the change
Four
[TestCase]rows that let aJavaScriptExceptionout of the body, run onnet472:Two of five cases reported, three vanished, and
dotnet testitself never returned — the first attempt waskilled at a ten-minute timeout, which is what a red
net472run looks like from the outside: a hang, not afailure. It cost an earlier session about forty minutes.
Same four rows, after:
with the message a reader actually wants:
Why it happens, and where the fix belongs
JavaScriptException.Locationhas beenref readonly SourceLocationsince #1270. Nothing that calls it byname notices. But a public property of an exception is read reflectively by everything that renders a
failed run — a test runner, a structured logger, an error page — and .NET Framework's
RuntimePropertyInfo.GetValueanswers a by-ref-returning property withNotSupportedException: ByRef return value not supported in reflection invocationinstead of dereferencingit the way .NET Core does. That is why only the
net472leg dies whilenet8.0andnet10.0reportnormally.
NUnit's
ExceptionHelper.AppendExceptionPropertiesdoes guard the getter — withcatch (TargetInvocationException). This exception is raised byGetValuebefore any invocation, so itis not a
TargetInvocationExceptionand it escapes; the worker thread unwinds and the run aborts. NUnitshould not be crashable by a getter's shape, whatever the library, and that half is filed upstream at
nunit/nunit#5401 — but waiting for it would leave every Jint
embedder on
net472with the same defect in their own logs, so the fix is Jint's.The change
SourceLocationis 24 bytes and the path is a thrown exception, so the reference bought nothing anything canmeasure. Reading the location is unchanged —
ex.Location,ex.Location.Start.Lineandex.Location != defaultall keep compiling; onlyref readonly var l = ref ex.Locationstops, which isdocs/v5-migration.md§3.18.ExceptionHelperalso walksInnerException, so the private nestedJavaScriptErrorWrapperExceptionisenumerated too. Its
Locationkeeps the by-ref accessor and turnsinternal, which is all it takes to dropout of
BindingFlags.Public.Locationgained the<summary>it never had, soUndocumentedPublicApi.txtshrinks by one.Public API impact
One line in each of the five baselines, and nothing else:
What holds it
Jint.Tests.PublicInterface/ExceptionPropertyReflectionTests.cs, two tests:Exceptionin the assembly, public or not, exposes a by-ref-returning public property.It fails on
net8.0andnet10.0as well, so a future one is caught on every leg rather than only on theone that would crash. Against unfixed code it names
Jint.Runtime.JavaScriptException.Location.of every inner exception, read each one) over a real thrown
JavaScriptException, and asserts the locationsurvives the by-value read rather than merely not throwing. Against unfixed code it fails on
net472withthe issue's exact
NotSupportedException.The four remaining by-ref public properties are on non-exception types —
Completion,CallFrame,DebugInformation,ExceptionThrownEventArgs— and stay as they are;Completion.Locationis genuinelyhot. The rule the test enforces is scoped to exceptions for that reason, and
Jint/AGENTS.mdsays so.Verification
dotnet build -c Release: clean.Jint.Tests: 11,601 passed onnet10.0andnet8.0, 8,221 onnet472, 0 failed.Jint.Tests.PublicInterface: 3,352 / 3,342 / 2,719 passed onnet10.0/net8.0/net472, 0 failed.Jint.Tests.CommonScripts(28 × 2) andJint.Tests.SourceGenerators(71): 0 failed.