Skip to content

fix(js/ts/python/dart/beam): align collection exception types with FSharp.Core - #5153

Merged
MangelMaxime merged 6 commits into
fable-compiler:mainfrom
leonitousconforti:fix-collection-exception-types
Oct 26, 2026
Merged

MangelMaxime merged 6 commits into
fable-compiler:mainfrom
leonitousconforti:fix-collection-exception-types

Conversation

@leonitousconforti

Copy link
Copy Markdown
Contributor

Follow-up to #5152, and stacked on it: this branch includes its commit until it merges.

With invalidArg and invalidOp raising typed exceptions, many collection functions turn out to raise a different type than FSharp.Core. I probed the error paths of List, Array, Seq, Map, Set and ranges on .NET and on the JS output, and this fixes the cases where the library already raises, but the wrong type or a plain Exception:

  • ArgumentException: reduce/reduceBack (and min/max/minBy/maxBy through them), List.last, Set.minElement/maxElement, length mismatches (zip, map2, fold2, transpose, ...), windowed, permute, a zero range step.
  • InvalidOperationException: list.Head/list.Tail (List.head/List.tail keep ArgumentException), take and splitAt past the end, Seq.skip past the end, Map and Set enumerators' Current outside the enumeration.
  • ArgumentOutOfRangeException: List.skip/Array.skip past the end, random*By with a randomizer outside [0, 1).
  • KeyNotFoundException: Array.find/findIndex/pick and their Back variants.
  • NotSupportedException: mutating a Map through IDictionary/ICollection.

Notes:

  • JS: Array.reduce/reduceBack no longer map to native reduce/reduceRight, which threw a TypeError on empty arrays. permute's bijection check works again (it skipped the holes of new Array(n)). PHP falls back to the JS replacements, so it keeps the old Head/Tail mapping.
  • Python: the F# modules and seq_native.py. The Rust-backed Array module is unchanged.
  • Beam: Seq.fs, Range.fs and the explicit raises in fable_list.erl/fable_map.erl. Functions that fail with a native Erlang error have no raise to retype, so they are unchanged.
  • Rust is unchanged.

Types and parameter names match .NET; messages follow FSharp.Core where it was cheap. The new tests assert types only.

Not covered: cases where .NET raises and Fable raises nothing (e.g. List.zip with different lengths, List.init -1), null checks, and array indexing (arr.[i] should raise IndexOutOfRangeException, but shares code with ResizeArray).

The full CI passes on my fork: https://github.com/leonitousconforti/Fable/actions/runs/40171511126. The JS suite also runs under JavaScriptCore with no regressions against #5152.

Now that invalidArg and invalidOp raise typed exceptions, List, Array,
Seq, Map, Set and range functions raise the exception types FSharp.Core
raises:

- reduce/reduceBack (and min/max/minBy/maxBy, Set.intersectMany) and
  List.last on empty input raise ArgumentException. Array.reduce and
  Array.reduceBack no longer map to the native JS reduce, which threw a
  TypeError.
- list.Head and list.Tail raise InvalidOperationException, while
  List.head and List.tail keep raising ArgumentException. PHP, which
  shares these replacements, keeps calling List.head and List.tail.
- take/splitAt past the end and Seq.skip raise InvalidOperationException;
  List.skip and Array.skip raise ArgumentOutOfRangeException. Seq.tail
  keeps raising ArgumentException.
- Array length mismatches, windowed, permute and zero range steps raise
  ArgumentException, and find/pick misses raise KeyNotFoundException.
  Array.permute now also rejects an index map that is not a bijection:
  its flags array had empty slots, which `every` skips.
- random*By with a randomizer outside [0, 1) raises
  ArgumentOutOfRangeException.
- Map and Set enumerators raise InvalidOperationException, and mutating
  a Map through IDictionary or ICollection raises NotSupportedException
  with FSharp.Core's message.

SR.inputWasEmpty now reads "The input list was empty." like FSharp.Core.
List, Seq, Set, Map and range functions raised a plain Exception or a
different System exception than FSharp.Core. They now match .NET:
reduce, min, max and last on empty input raise ArgumentException,
Seq.head on empty input and Seq.item past the end raise
ArgumentException, take, splitAt and Seq.skip past the end raise
InvalidOperationException, List.skip past the end raises
ArgumentOutOfRangeException, a zero range step raises
ArgumentException, and the Map and Set enumerators raise
InvalidOperationException outside the enumeration. Map's ICollection
mutators raise NotSupportedException with FSharp.Core's message.

list.Head and list.Tail raise InvalidOperationException while List.head
and List.tail keep raising ArgumentException, so Replacements now sends
the properties to the FSharpList members. Seq.tail gets its own
implementation so it keeps raising ArgumentException.

The List messages pick up FSharp.Core's "The input list was empty."
from the Global.fs shared with fable-library-ts.
List, Array, Seq, Set, Map and range functions raised untyped errors
(failwith, or Dart's own StateError for Array.reduce and Array.find) or
a different typed exception than FSharp.Core does on .NET. They now raise
the same exception types:

- reduce/min/max on empty inputs, List.last, List.head/tail and
  Set.minElement/maxElement: ArgumentException
- list.Head/list.Tail on an empty list: InvalidOperationException
- take/splitAt past the end and Seq.skip: InvalidOperationException
  (Seq.tail keeps ArgumentException)
- List.skip/Array.skip past the end and an out-of-range randomizer:
  ArgumentOutOfRangeException
- Array length mismatches, windowed, permute and a zero range step:
  ArgumentException
- Array.find/findBack/findIndex/findIndexBack/pick: KeyNotFoundException
- Map/Set enumerator Current outside the enumeration:
  InvalidOperationException

SR.inputWasEmpty now reads "The input list was empty." like FSharp.Core.
Collection functions raised untyped errors, or the wrong exception type,
where FSharp.Core raises a typed one, so handlers like
`:? InvalidOperationException` never caught them:

- Seq.reduce, Seq.reduceBack (and Seq.min/max/minBy/maxBy) on an empty
  sequence raise ArgumentException instead of InvalidOperationException.
- Seq.take and Seq.skip past the end raise InvalidOperationException
  instead of ArgumentException. Seq.tail gets its own implementation so
  it keeps raising ArgumentException on an empty sequence.
- List.take and Array.take past the end raise InvalidOperationException.
- List/Array find, findBack, findIndex, findIndexBack and pick raise
  KeyNotFoundException.
- Map.findKey and Map.pick raise KeyNotFoundException when nothing
  matches.
- The random*By functions raise ArgumentOutOfRangeException for a
  randomizer outside [0, 1), and ArgumentException for an empty source,
  a negative count or a sample larger than the source.
- A range with a zero step raises ArgumentException.
@MangelMaxime
MangelMaxime force-pushed the fix-collection-exception-types branch from a4dfc6e to d6ac902 Compare October 26, 2026 19:44
@MangelMaxime
MangelMaxime merged commit 7d7ceb3 into fable-compiler:main Oct 26, 2026
36 checks passed
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.

2 participants