diff --git a/src/Ramstack.Parsing/Parser`1.cs b/src/Ramstack.Parsing/Parser`1.cs
index ceb75e0..5a4cfd4 100644
--- a/src/Ramstack.Parsing/Parser`1.cs
+++ b/src/Ramstack.Parsing/Parser`1.cs
@@ -34,6 +34,18 @@ protected Parser(string? name) =>
///
/// if the parser succeeded; otherwise, .
///
+ ///
+ ///
+ /// Any failed match returns , including a forced termination
+ /// through or .
+ ///
+ ///
+ /// Exceptions thrown by user-provided functions, such as the callbacks passed to
+ /// Map, Do, or Fold, are not suppressed and propagate to the caller.
+ /// Use to capture them in
+ /// instead.
+ ///
+ ///
public bool TryParse(ReadOnlySpan source, [NotNullWhen(true)] out T? value)
{
try
@@ -46,9 +58,9 @@ public bool TryParse(ReadOnlySpan source, [NotNullWhen(true)] out T? value
if (TryParse(ref context, out value))
return true;
}
- catch
+ catch (FatalErrorException)
{
- // Ignore exceptions
+ // A forced parse error is an ordinary failed match for the boolean API.
}
value = default;
diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs
new file mode 100644
index 0000000..c838763
--- /dev/null
+++ b/tests/Ramstack.Parsing.Tests/ParsersTests.Exceptions.cs
@@ -0,0 +1,82 @@
+using static Ramstack.Parsing.Parser;
+
+namespace Ramstack.Parsing;
+
+partial class ParsersTests
+{
+ [Test]
+ public void TryParse_FatalErrorParser_ReturnsFalse()
+ {
+ var parser = Fail("failure message");
+
+ Assert.That(parser.TryParse("x", out var value), Is.False);
+ Assert.That(value, Is.EqualTo('\0'));
+ }
+
+ [Test]
+ public void TryParse_FatalErrorCallback_ReturnsFalse()
+ {
+ var parser = L('a').Do(c =>
+ {
+ if (c == 'a')
+ FatalError("forced failure");
+
+ return c;
+ });
+
+ Assert.That(parser.TryParse("a", out var value), Is.False);
+ Assert.That(value, Is.EqualTo('\0'));
+
+ var result = parser.Parse("a");
+
+ Assert.That(result.ErrorMessage, Is.EqualTo("(1:2) forced failure"));
+ Assert.That(result.Exception, Is.Null);
+ }
+
+ [Test]
+ public void TryParse_FailedMatch_ReturnsFalse()
+ {
+ var parser = L('a');
+
+ Assert.That(parser.TryParse("b", out var value), Is.False);
+ Assert.That(value, Is.EqualTo('\0'));
+ }
+
+ [Test]
+ public void TryParse_UserCallbackThrows_PropagatesException()
+ {
+ var parser = L('a').Map(_ => throw new InvalidOperationException("callback failure"));
+
+ Assert.That(
+ () => parser.TryParse("a", out _),
+ Throws
+ .TypeOf()
+ .With.Message.EqualTo("callback failure"));
+ }
+
+ [Test]
+ public void TryParse_UninitializedDeferredParser_ReturnsFalse()
+ {
+ var parser = Deferred();
+
+ Assert.That(parser.TryParse("x", out var value), Is.False);
+ Assert.That(value, Is.EqualTo('\0'));
+
+ Assert.That(
+ parser.Parse("x").ErrorMessage,
+ Is.EqualTo("(1:1) The deferred parser has not been initialized."));
+ }
+
+ [Test]
+ public void Parse_UserCallbackThrows_ReportsException()
+ {
+ var parser = L('a').Map(_ => throw new InvalidOperationException("callback failure"));
+
+ var result = parser.Parse("a");
+
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Exception, Is.TypeOf());
+ Assert.That(result.Exception!.Message, Is.EqualTo("callback failure"));
+ Assert.That(result.ErrorMessage, Does.Contain("callback failure"));
+ }
+}