From df177a610a047c05d24f527449f008387edd3c05 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Fri, 23 Oct 2026 10:41:22 -0500 Subject: [PATCH 1/3] fix(js/ts/python/dart/beam): raise typed exceptions from invalidArg and invalidOp `invalidArg` and `invalidOp` threw a plain `Exception`, so `:? ArgumentException` and `:? InvalidOperationException` handlers never caught them, nor the library errors raised through them. They now go through `FSharp.Core.Operators` like `nullArg`, raising the `System` exceptions the libraries already define. Beam tags its exception maps inline, as it does for those exceptions' constructors. --- src/Fable.Transforms/Beam/Replacements.fs | 9 ++++++--- src/Fable.Transforms/Dart/Replacements.fs | 8 ++------ src/Fable.Transforms/Python/Replacements.fs | 9 +++------ src/Fable.Transforms/Replacements.fs | 8 ++------ src/fable-library-dart/FSharp.Core.fs | 8 ++++++++ src/fable-library-ts/FSharp.Core.fs | 8 ++++++++ tests/Beam/ExceptionTests.fs | 20 ++++++++++++++++++++ tests/Dart/src/MiscTests.fs | 18 ++++++++++++++++++ tests/Js/Main/MiscTests.fs | 18 ++++++++++++++++++ tests/Python/TestMisc.fs | 20 ++++++++++++++++++++ 10 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index f590ef4455..2253293e98 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -329,13 +329,16 @@ let private operators | ("DefaultArg" | "DefaultValueArg"), [ opt; defValue ] -> Helper.LibCall(com, "fable_option", "default_value", _t, [ opt; defValue ]) |> Some - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r _t msg |> Some + | "FailWith", [ msg ] -> makeThrow r _t msg |> Some + | "InvalidOp", [ msg ] -> + 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 msg |> Some + makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_exception, message => $0}") + |> Some | "Raise", [ arg ] -> makeThrow r _t arg |> Some | "NullArg", [ arg ] -> let msg = diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 86ea19665a..65a3c7311e 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1036,16 +1036,12 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> Extended(Throw(None, t), r) |> Some // Math functions diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index e404a973fb..248e3266ef 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1216,18 +1216,15 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // bprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "NullArg" | "Using"), _ -> + fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args | "Lock", _ -> Helper.LibCall(com, "util", "lock", t, args, i.SignatureArgTypes, ?loc = r) |> Some // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> match ctx.CaughtException with diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 2af2d5e382..8109a1ae2a 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1562,16 +1562,12 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> match ctx.CaughtException with diff --git a/src/fable-library-dart/FSharp.Core.fs b/src/fable-library-dart/FSharp.Core.fs index 71ae90e717..89cdb1338a 100644 --- a/src/fable-library-dart/FSharp.Core.fs +++ b/src/fable-library-dart/FSharp.Core.fs @@ -14,6 +14,14 @@ module Operators = let nullArg (argumentName: string) = raise (System.ArgumentNullException(argumentName)) + [] + let invalidArg (argumentName: string) (message: string) = + raise (System.ArgumentException(message, argumentName)) + + [] + let invalidOp (message: string) = + raise (System.InvalidOperationException(message)) + [] let using<'T, 'R when 'T :> System.IDisposable and 'T: null> (resource: 'T) (action: 'T -> 'R) = try diff --git a/src/fable-library-ts/FSharp.Core.fs b/src/fable-library-ts/FSharp.Core.fs index 4257e6aed2..42a163060c 100644 --- a/src/fable-library-ts/FSharp.Core.fs +++ b/src/fable-library-ts/FSharp.Core.fs @@ -41,6 +41,14 @@ module Operators = let nullArg (argumentName: string) = raise (System.ArgumentNullException(argumentName)) + [] + let invalidArg (argumentName: string) (message: string) = + raise (System.ArgumentException(message, argumentName)) + + [] + let invalidOp (message: string) = + raise (System.InvalidOperationException(message)) + [] let using<'T, 'R when 'T :> System.IDisposable and 'T: null> (resource: 'T) (action: 'T -> 'R) = try diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index 49413902e5..efd000fa8c 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -170,3 +170,23 @@ let ``test invalidArg formats the message like .NET`` () = throwsError "This is invalid (Parameter 'arg')" (fun () -> invalidArg "arg" "This is invalid" ) + +[] +let ``test invalidArg raises ArgumentException`` () = + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid (Parameter 'arg')" + +[] +let ``test invalidOp raises InvalidOperationException`` () = + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid" diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index c6278ff2ec..fb0c2a645e 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -155,6 +155,24 @@ let tests() = invalidArg "arg" "This is invalid" ) + testCase "invalidArg raises ArgumentException" <| fun () -> + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid (Parameter 'arg')" caught + + testCase "invalidOp raises InvalidOperationException" <| fun () -> + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid" caught + testCase "Compiler target flags have correct value per target" <| fun () -> equal false Compiler.isJavaScript equal false Compiler.isTypeScript diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 5b3e359150..910b403556 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -1260,6 +1260,24 @@ let tests = invalidArg "arg" "This is invalid" ) + testCase "invalidArg raises ArgumentException" <| fun () -> + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid (Parameter 'arg')" caught + + testCase "invalidOp raises InvalidOperationException" <| fun () -> + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid" caught + testCase "Pattern matching optimization works (switch statement)" <| fun () -> let mutable x = "" let i = 4 diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 44eca8a4cc..0fe681a1f1 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1277,6 +1277,26 @@ let ``test invalidArg formats the message like .NET`` () = invalidArg "arg" "This is invalid" ) +[] +let ``test invalidArg raises ArgumentException`` () = + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid (Parameter 'arg')" + +[] +let ``test invalidOp raises InvalidOperationException`` () = + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid" + # nowarn "26" // This rule will never be matched (code 26) [] From 67d67a9394b81515ef4144444941f5b59580e66d Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 17:38:41 +0100 Subject: [PATCH 2/3] fix(beam): raise ArgumentNullException from nullArg --- src/Fable.Transforms/Beam/Replacements.fs | 3 ++- tests/Beam/ExceptionTests.fs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 2253293e98..38f890947c 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -346,7 +346,8 @@ let private operators (Value(StringConstant "Value cannot be null. (Parameter '", None)) (add arg (Value(StringConstant "')", None))) - makeThrow r _t msg |> Some + makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_null_exception, message => $0}") + |> Some | "IsNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =:= undefined)" |> Some | "IsNotNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =/= undefined)" |> Some // Nullable active patterns and helpers diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index efd000fa8c..d6e9119c2b 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -190,3 +190,13 @@ let ``test invalidOp raises InvalidOperationException`` () = | :? System.InvalidOperationException as e -> e.Message | e -> "Unexpected exception: " + e.Message caught |> equal "This is invalid" + +[] +let ``test nullArg raises ArgumentNullException`` () = + let caught = + try + nullArg "arg" + with + | :? System.ArgumentNullException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "Value cannot be null. (Parameter 'arg')" From bce122bebbad0756aff64a83dd54376200dc7deb Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 18:54:04 +0100 Subject: [PATCH 3/3] fix(beam): keep parameter name in ArgumentException --- src/Fable.Transforms/Beam/Replacements.fs | 52 +++++++++++++++-------- src/fable-library-beam/fable_utils.erl | 9 ++++ tests/Beam/MiscTests.fs | 32 ++++++++++++++ 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 38f890947c..4b3df7f340 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -6227,25 +6227,43 @@ let tryCall let typeTag = typeName.Substring(typeName.LastIndexOf('.') + 1) |> sanitizeErlangName + let make msg inner = + match inner with + | None -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" + | 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) + let paramNameFirst = + typeName = "System.ArgumentNullException" + || typeName = "System.ArgumentOutOfRangeException" + + let defaultMessage = + match typeName with + | "System.ArgumentException" -> "Value does not fall within the expected range." + | "System.ArgumentNullException" -> "Value cannot be null." + | "System.ArgumentOutOfRangeException" -> "Specified argument was out of the range of valid values." + | _ -> $"Exception of type '%s{typeName}' was thrown." + match info.CompiledName, thisArg, args with - | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" |> Some - | ".ctor", None, [ msg; second ] -> + | ".ctor", None, [] -> make (makeStrConst defaultMessage) None |> Some + | ".ctor", None, [ paramName ] when paramNameFirst -> + make (withParamName (makeStrConst defaultMessage) paramName) None |> Some + | ".ctor", None, [ msg ] -> make msg None |> Some + | ".ctor", None, [ first; second ] -> match info.SignatureArgTypes with - // (message, paramName): paramName is not modelled, only the message is kept - | [ _; String ] -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" |> Some - // (message, innerException) - | _ -> - emitExpr r t [ msg; second ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - |> Some - // (message, paramName, innerException) - | ".ctor", None, [ msg; _paramName; inner ] -> - emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - |> Some - | ".ctor", None, [] -> - let msg = $"Exception of type '%s{typeName}' was thrown." - - emitExpr r t [] $"#{{exn_type => %s{typeTag}, message => <<\"%s{msg}\">>}}" - |> Some + | [ _; String ] when paramNameFirst -> make (withParamName second first) None |> Some + | [ _; String ] when isArgumentException -> make (withParamName 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 + | ".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 | _ -> None diff --git a/src/fable-library-beam/fable_utils.erl b/src/fable-library-beam/fable_utils.erl index bcca5e5d90..6d5c72ac1b 100644 --- a/src/fable-library-beam/fable_utils.erl +++ b/src/fable-library-beam/fable_utils.erl @@ -25,6 +25,7 @@ to_list/1, enumerate_to_list/1, aggregate_exception/2, + argument_exception_message/2, exception_message/1, is_exception_type/2, new_byte_array/1, @@ -67,6 +68,7 @@ -spec to_list(term()) -> list(). -spec enumerate_to_list(term()) -> list(). -spec aggregate_exception(binary(), term()) -> map(). +-spec argument_exception_message(binary(), binary() | undefined) -> binary(). -spec exception_message(term()) -> binary(). -spec is_exception_type(term(), atom()) -> boolean(). -spec new_byte_array(list() | binary() | tuple()) -> tuple(). @@ -477,6 +479,13 @@ aggregate_exception(BaseMessage, ExceptionsValue) -> inner_exceptions => new_ref(Exceptions) }. +argument_exception_message(Message, undefined) -> + Message; +argument_exception_message(Message, <<>>) -> + Message; +argument_exception_message(Message, ParamName) -> + <>. + %% Built-in .NET exceptions carry a compact nominal tag. Follow the .NET exception %% hierarchy here so a catch for a base exception also accepts its derived exceptions. is_exception_type(Exception, Expected) when is_reference(Exception) -> diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index 2b14e17b7b..764d23b0e0 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -768,6 +768,38 @@ let ``test ArgumentException with message and inner exception works`` () = ex.Message |> equal "outer message" ex.InnerException.Message |> equal "the inner cause" +[] +let ``test ArgumentException default message matches .NET`` () = + let ex = System.ArgumentException() + ex.Message |> equal "Value does not fall within the expected range." + +[] +let ``test ArgumentException with message and parameter name works`` () = + let ex = System.ArgumentException("outer message", "arg") + ex.Message |> equal "outer message (Parameter 'arg')" + +[] +let ``test ArgumentException with message, parameter name and inner exception works`` () = + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", "arg", inner) + ex.Message |> equal "outer message (Parameter 'arg')" + ex.InnerException.Message |> equal "the inner cause" + +[] +let ``test ArgumentNullException with parameter name works`` () = + let ex = System.ArgumentNullException("arg") + ex.Message |> equal "Value cannot be null. (Parameter 'arg')" + +[] +let ``test ArgumentNullException with parameter name and message works`` () = + let ex = System.ArgumentNullException("arg", "custom message") + ex.Message |> equal "custom message (Parameter 'arg')" + +[] +let ``test ArgumentOutOfRangeException with parameter name works`` () = + let ex = System.ArgumentOutOfRangeException("arg") + ex.Message |> equal "Specified argument was out of the range of valid values. (Parameter 'arg')" + [] let ``test Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner")