From 8bfcebde6639175c85ae47f23b06f58bda3fb113 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Tue, 27 Oct 2026 17:52:03 -0600 Subject: [PATCH 1/5] feat(js/ts): support ArgumentException.ParamName Map ArgumentException.ParamName (also read through ArgumentNullException and ArgumentOutOfRangeException) to the getter the library already has, instead of failing to compile. Constructors without a parameter name now store null rather than "", so an absent ParamName reads as null like on .NET. Messages are unchanged. --- src/Fable.Transforms/Replacements.fs | 1 + src/fable-library-ts/System.fs | 18 +++++++++++------- tests/Js/Main/MiscTests.fs | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index bb1331fb10..9a930256d5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3334,6 +3334,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> getFieldWith r t e "message" |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/fable-library-ts/System.fs b/src/fable-library-ts/System.fs index 03cb86a83d..86182aa495 100644 --- a/src/fable-library-ts/System.fs +++ b/src/fable-library-ts/System.fs @@ -74,21 +74,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 6df4c81f2c..94153e6d16 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -510,6 +510,13 @@ let buildRecordWithMutableFnWithFor (xs: RecordWithMutableFn list) = let x = xs[i] items <- (x.MutableFn >>* ()) :: items items + +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + let tests = testList "Miscellaneous" [ testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031 @@ -1269,6 +1276,26 @@ let tests = | e -> "Unexpected exception: " + e.Message equal "This is invalid (Parameter 'arg')" caught + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + testCase "invalidOp raises InvalidOperationException" <| fun () -> let caught = try From 76020b80a3869084ee95adec889978fc643f032a Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Tue, 27 Oct 2026 17:52:49 -0600 Subject: [PATCH 2/5] feat(dart): support ArgumentException.ParamName Map ArgumentException.ParamName to the getter the Dart library already has, instead of failing to compile. Dart strings are not nullable, so an absent parameter name keeps reading as an empty string. --- src/Fable.Transforms/Dart/Replacements.fs | 1 + tests/Dart/src/MiscTests.fs | 27 +++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 354372048a..ce6705fd06 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -2721,6 +2721,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> Helper.InstanceCall(e, "toString", t, [], ?loc = r) |> Some // | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index fb0c2a645e..e532689428 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -63,6 +63,12 @@ type ElmishWidget<'Model, 'View, 'Msg>(view: 'Model -> ('Msg -> unit) -> 'View) let _view = view member _.Render(model, dispatch) = _view model dispatch +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + let tests() = testCase "ref works" <| fun () -> let x = ref 5 @@ -164,6 +170,27 @@ let tests() = | e -> "Unexpected exception: " + e.Message equal "This is invalid (Parameter 'arg')" caught + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + // Dart strings are not nullable, so an absent name reads as an empty string + System.ArgumentException("m").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", exn "i").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> System.String.IsNullOrEmpty |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + testCase "invalidOp raises InvalidOperationException" <| fun () -> let caught = try From e94755235971708f8e7ef70ffc7f32bed6039416 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Tue, 27 Oct 2026 17:52:56 -0600 Subject: [PATCH 3/5] feat(python): support ArgumentException.ParamName Map ArgumentException.ParamName (also read through ArgumentNullException and ArgumentOutOfRangeException) to the getter the library already has, instead of failing to compile. Constructors without a parameter name now store None rather than "", so an absent ParamName reads as null like on .NET. Messages are unchanged. --- src/Fable.Transforms/Python/Replacements.fs | 1 + src/fable-library-py/fable_library/System.fs | 18 ++++++++----- tests/Python/TestMisc.fs | 28 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 3da6ef0dfd..0f9ae88991 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2922,6 +2922,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr // Caught exceptions are typed as Python's built-in Exception, which lacks this attribute. Helper.GlobalCall("getattr", t, [ e; makeStrConst "inner_exception" ], ?loc = r) |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/fable-library-py/fable_library/System.fs b/src/fable-library-py/fable_library/System.fs index a85c921c46..ca77ee92ae 100644 --- a/src/fable-library-py/fable_library/System.fs +++ b/src/fable-library-py/fable_library/System.fs @@ -76,21 +76,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 0fe681a1f1..ab47fc7125 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1287,6 +1287,34 @@ let ``test invalidArg raises ArgumentException`` () = | e -> "Unexpected exception: " + e.Message caught |> equal "This is invalid (Parameter 'arg')" +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + [] let ``test invalidOp raises InvalidOperationException`` () = let caught = From b60a3d5d33c3984f0010fa3881982dbde8f69707 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Tue, 27 Oct 2026 17:53:45 -0600 Subject: [PATCH 4/5] feat(beam): support ArgumentException.ParamName Argument exception maps now carry a param_name key, which ArgumentException.ParamName reads (undefined, so null, when there is none). The compiler adds it for invalidArg, nullArg and the ArgumentException, ArgumentNullException and ArgumentOutOfRangeException constructors that take a parameter name, evaluating the name once, and the library adds it where fable_list, fable_resize_array and fable_int raise argument exceptions (fable_decimal already did). Messages are unchanged, except that ArgumentOutOfRangeException(paramName, actualValue, message) no longer uses the parameter name as its message and the message as its inner exception. --- src/Fable.Transforms/Beam/Replacements.fs | 88 +++++++++++++++---- src/fable-library-beam/fable_int.erl | 6 +- src/fable-library-beam/fable_list.erl | 6 +- src/fable-library-beam/fable_resize_array.erl | 3 +- tests/Beam/ExceptionTests.fs | 41 +++++++++ 5 files changed, 122 insertions(+), 22 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index c0c94f7b81..d668b28e4e 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -334,19 +334,49 @@ let private operators makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => invalid_operation_exception, message => $0}") |> Some | "InvalidArg", [ argName; msg ] -> - let msg = - add msg (add (add (Value(StringConstant " (Parameter '", None)) argName) (Value(StringConstant "')", None))) - - makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_exception, message => $0}") + captureExprOnce + com + "param_name" + argName + (fun argName -> + let msg = + add + msg + (add + (add (Value(StringConstant " (Parameter '", None)) argName) + (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; argName ] + "#{exn_type => argument_exception, message => $0, param_name => $1}") + ) |> Some | "Raise", [ arg ] -> makeThrow r _t arg |> Some | "NullArg", [ arg ] -> - let msg = - add - (Value(StringConstant "Value cannot be null. (Parameter '", None)) - (add arg (Value(StringConstant "')", None))) - - makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_null_exception, message => $0}") + captureExprOnce + com + "param_name" + arg + (fun arg -> + let msg = + add + (Value(StringConstant "Value cannot be null. (Parameter '", None)) + (add arg (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; arg ] + "#{exn_type => argument_null_exception, message => $0, param_name => $1}") + ) |> Some | "IsNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =:= undefined)" |> Some | "IsNotNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =/= undefined)" |> Some @@ -6274,9 +6304,6 @@ let tryCall | Some inner -> emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - let withParamName msg paramName = - Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) - let isArgumentException = typeName = "System.ArgumentException" // ArgumentNullException and ArgumentOutOfRangeException take (paramName, message), the others (message, paramName) @@ -6284,6 +6311,31 @@ let tryCall typeName = "System.ArgumentNullException" || typeName = "System.ArgumentOutOfRangeException" + // Argument exceptions also keep the parameter name for ParamName, so both are captured once in source order + let makeArg msg paramName inner = + let build msg paramName = + let msg = + Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) + + match inner with + | None -> + emitExpr r t [ msg; paramName ] $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1}}" + | Some inner -> + emitExpr + r + t + [ msg; paramName; inner ] + $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1, inner_exception => $2}}" + + if paramNameFirst then + captureExprs2Once + com + ("param_name", paramName) + ("message", msg) + (fun paramName msg -> build msg paramName) + else + captureExprs2Once com ("message", msg) ("param_name", paramName) build + let defaultMessage = match typeName with | "System.ArgumentException" -> "Value does not fall within the expected range." @@ -6294,19 +6346,21 @@ let tryCall match info.CompiledName, thisArg, args with | ".ctor", None, [] -> make (makeStrConst defaultMessage) None |> Some | ".ctor", None, [ paramName ] when paramNameFirst -> - make (withParamName (makeStrConst defaultMessage) paramName) None |> Some + makeArg (makeStrConst defaultMessage) paramName None |> Some | ".ctor", None, [ msg ] -> make msg None |> Some | ".ctor", None, [ first; second ] -> match info.SignatureArgTypes with - | [ _; String ] when paramNameFirst -> make (withParamName second first) None |> Some - | [ _; String ] when isArgumentException -> make (withParamName first second) None |> Some + | [ _; String ] when paramNameFirst -> makeArg second first None |> Some + | [ _; String ] when isArgumentException -> makeArg first second None |> Some | [ _; String ] -> make first None |> Some | _ -> make first (Some second) |> Some | ".ctor", None, [ msg; paramName; inner ] when isArgumentException -> - make (withParamName msg paramName) (Some inner) |> Some + makeArg msg paramName (Some inner) |> Some + | ".ctor", None, [ paramName; _; msg ] when paramNameFirst -> makeArg msg paramName None |> Some | ".ctor", None, [ msg; _; inner ] -> make msg (Some inner) |> Some | "get_Message", Some c, _ -> emitExpr r t [ c ] "maps:get(message, $0, $0)" |> Some | "get_InnerException", Some c, _ -> emitExpr r t [ c ] "maps:get(inner_exception, $0, undefined)" |> Some + | "get_ParamName", Some c, _ -> emitExpr r t [ c ] "maps:get(param_name, $0, undefined)" |> Some | _ -> None // System.Type (reflection) — type info is a map #{fullname => ..., generics => [...]} | "System.Type" -> diff --git a/src/fable-library-beam/fable_int.erl b/src/fable-library-beam/fable_int.erl index 9113fa354b..d75ed94194 100644 --- a/src/fable-library-beam/fable_int.erl +++ b/src/fable-library-beam/fable_int.erl @@ -87,7 +87,8 @@ wrap_u64(N) -> log2(N) when N < 0 -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => <<"Non-negative number required. (Parameter 'value')">> + message => <<"Non-negative number required. (Parameter 'value')">>, + param_name => <<"value">> }); log2(0) -> 0; @@ -140,7 +141,8 @@ bigint_from_byte_array(BytesValue) -> bigint_pow(_Base, Exponent) when Exponent < 0 -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">> + message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">>, + param_name => <<"exponent">> }); bigint_pow(Base, Exponent) -> bigint_pow(Base, Exponent, 1). diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index ea7d918ecb..34962b8cd9 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -909,13 +909,15 @@ randomizer_out_of_range(R) -> "The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0). (Parameter 'randomizer')", [R] ) - ) + ), + param_name => <<"randomizer">> }). invalid_arg(ParamName, Message) -> erlang:error(#{ exn_type => argument_exception, - message => fable_utils:argument_exception_message(Message, ParamName) + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName }). argument_out_of_range(ParamName, Message) -> diff --git a/src/fable-library-beam/fable_resize_array.erl b/src/fable-library-beam/fable_resize_array.erl index c24a6df568..ec6335786d 100644 --- a/src/fable-library-beam/fable_resize_array.erl +++ b/src/fable-library-beam/fable_resize_array.erl @@ -168,7 +168,8 @@ non_negative_number_required(ParamName) -> erlang:error(#{ exn_type => argument_out_of_range_exception, message => - fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName) + fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName), + param_name => ParamName }). %% Find first matching predicate, return Default when not found diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index d6e9119c2b..ec4dafbeff 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -200,3 +200,44 @@ let ``test nullArg raises ArgumentNullException`` () = | :? System.ArgumentNullException as e -> e.Message | e -> "Unexpected exception: " + e.Message caught |> equal "Value cannot be null. (Parameter 'arg')" + +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", box 5, "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.exactlyOne [ 1; 2 ] |> ignore) |> equal "source" + paramName (fun () -> Seq.head Seq.empty |> ignore) |> equal "source" + paramName (fun () -> System.Array.BinarySearch([| 1; 3; 5 |], -1, 2, 5) |> ignore) |> equal "index" + +[] +let ``test ArgumentException parameter name is evaluated once`` () = + let calls = ref 0 + let name () = + calls.Value <- calls.Value + 1 + "x" + paramName (fun () -> invalidArg (name ()) "m") |> equal "x" + paramName (fun () -> nullArg (name ())) |> equal "x" + System.ArgumentException("m", name ()).ParamName |> equal "x" + System.ArgumentNullException(name (), "m").ParamName |> equal "x" + calls.Value |> equal 4 From 6afb7785cf008774a6b8136fff0bfaab8b29f139 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 29 Oct 2026 19:49:16 +0100 Subject: [PATCH 5/5] fix(beam): keep ParamName in argument_out_of_range --- src/Fable.Transforms/Beam/Replacements.fs | 2 +- src/fable-library-beam/fable_list.erl | 3 ++- tests/Beam/ExceptionTests.fs | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index d668b28e4e..11b28ecf45 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -6311,7 +6311,7 @@ let tryCall typeName = "System.ArgumentNullException" || typeName = "System.ArgumentOutOfRangeException" - // Argument exceptions also keep the parameter name for ParamName, so both are captured once in source order + // message and paramName are evaluated once, in source order let makeArg msg paramName inner = let build msg paramName = let msg = diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 34962b8cd9..1cc2b2c97e 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -923,7 +923,8 @@ invalid_arg(ParamName, Message) -> argument_out_of_range(ParamName, Message) -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => fable_utils:argument_exception_message(Message, ParamName) + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName }). lists_had_different_lengths(ParamName) -> diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index ec4dafbeff..b9bf615daa 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -229,6 +229,8 @@ let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = paramName (fun () -> List.exactlyOne [ 1; 2 ] |> ignore) |> equal "source" paramName (fun () -> Seq.head Seq.empty |> ignore) |> equal "source" paramName (fun () -> System.Array.BinarySearch([| 1; 3; 5 |], -1, 2, 5) |> ignore) |> equal "index" + paramName (fun () -> Array.sub [| 1 |] 0 5 |> ignore) |> equal "count" + paramName (fun () -> Array.sub [| 1 |] -1 1 |> ignore) |> equal "startIndex" [] let ``test ArgumentException parameter name is evaluated once`` () =