[prev in list] [next in list] [prev in thread] [next in thread] 

List:       jakarta-commons-dev
Subject:    [commons-geometry] branch master updated: GEOMETRY-138: switching to unchecked exceptions in IO modu
From:       mattjuntunen () apache ! org
Date:       2021-07-31 22:15:59
Message-ID: 162776975961.31057.17781776936572162146 () gitbox ! apache ! org
[Download RAW message or body]

This is an automated email from the ASF dual-hosted git repository.

mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git


The following commit(s) were added to refs/heads/master by this push:
     new eddfd08  GEOMETRY-138: switching to unchecked exceptions in IO modules
eddfd08 is described below

commit eddfd0823e36ac29044ee5eac371da62fda0957b
Author: Matt Juntunen <mattjuntunen@apache.org>
AuthorDate: Sat Jul 31 07:44:49 2021 -0400

    GEOMETRY-138: switching to unchecked exceptions in IO modules
---
 .../geometry/io/core/BoundaryIOManager.java        |  53 +++--
 .../geometry/io/core/BoundaryReadHandler.java      |  26 ++-
 .../geometry/io/core/BoundaryWriteHandler.java     |   6 +-
 .../geometry/io/core/input/FileGeometryInput.java  |   5 +-
 .../geometry/io/core/input/GeometryInput.java      |   5 +-
 .../geometry/io/core/input/UrlGeometryInput.java   |   5 +-
 .../geometry/io/core/internal/CharReadBuffer.java  |  76 ++++---
 .../geometry/io/core/internal/GeometryIOUtils.java | 155 +++++++++++--
 .../geometry/io/core/internal/IOConsumer.java      |  21 +-
 .../geometry/io/core/internal/IOToIntFunction.java |  22 +-
 .../io/core/internal/SimpleTextParser.java         | 229 +++++++++-----------
 .../io/core/output/FileGeometryOutput.java         |   5 +-
 .../geometry/io/core/output/GeometryOutput.java    |   5 +-
 .../io/core/utils/AbstractTextFormatWriter.java    |  38 ++--
 .../geometry/io/core/BoundaryIOManagerTest.java    |  30 ++-
 .../io/core/input/UrlGeometryInputTest.java        |   2 +-
 .../io/core/internal/CharReadBufferTest.java       |  73 +++++--
 .../io/core/internal/GeometryIOUtilsTest.java      | 201 ++++++++++++++++-
 .../io/core/internal/SimpleTextParserTest.java     | 196 +++++++++--------
 .../core/utils/AbstractTextFormatWriterTest.java   |  40 +++-
 .../threed/AbstractBoundaryReadHandler3D.java      |  24 +-
 .../threed/AbstractBoundaryWriteHandler3D.java     |   7 +-
 .../io/euclidean/threed/BoundaryIOManager3D.java   |  58 ++---
 .../io/euclidean/threed/BoundaryReadHandler3D.java |  23 +-
 .../euclidean/threed/BoundaryWriteHandler3D.java   |  13 +-
 .../io/euclidean/threed/FacetDefinitionReader.java |  16 +-
 .../commons/geometry/io/euclidean/threed/IO3D.java | 241 ++++++++++++---------
 .../io/euclidean/threed/obj/AbstractObjParser.java |  42 ++--
 .../threed/obj/AbstractObjPolygonReader.java       |  19 +-
 .../threed/obj/ObjBoundaryReadHandler3D.java       |  21 +-
 .../threed/obj/ObjBoundaryWriteHandler3D.java      |  23 +-
 .../threed/obj/ObjFacetDefinitionReader.java       |   3 +-
 .../threed/obj/ObjTriangleMeshReader.java          |   6 +-
 .../io/euclidean/threed/obj/ObjWriter.java         |  78 ++++---
 .../io/euclidean/threed/obj/PolygonObjParser.java  |  25 +--
 .../threed/stl/BinaryStlFacetDefinitionReader.java |  47 ++--
 .../io/euclidean/threed/stl/BinaryStlWriter.java   |  29 ++-
 .../threed/stl/StlBoundaryReadHandler3D.java       |   3 +-
 .../threed/stl/StlBoundaryWriteHandler3D.java      |  36 +--
 .../threed/stl/StlFacetDefinitionReaders.java      |  14 +-
 .../threed/stl/TextStlFacetDefinitionReader.java   |  49 +++--
 .../io/euclidean/threed/stl/TextStlWriter.java     |  46 ++--
 .../txt/AbstractTextBoundaryWriteHandler3D.java    |  22 +-
 .../threed/txt/CsvBoundaryWriteHandler3D.java      |   4 +-
 .../threed/txt/TextBoundaryReadHandler3D.java      |  17 +-
 .../threed/txt/TextBoundaryWriteHandler3D.java     |   4 +-
 .../threed/txt/TextFacetDefinitionReader.java      |  38 ++--
 .../threed/txt/TextFacetDefinitionWriter.java      |  29 ++-
 .../io/euclidean/DocumentationExamplesTest.java    |   3 +-
 .../io/euclidean/EuclideanIOTestUtils.java         |   5 +-
 .../threed/AbstractBoundaryReadHandler3DTest.java  |  20 +-
 .../euclidean/threed/BoundaryIOManager3DTest.java  |  70 +++---
 .../geometry/io/euclidean/threed/IO3DTest.java     |  18 +-
 .../threed/obj/ObjBoundaryReadHandler3DTest.java   |  13 +-
 .../threed/obj/ObjBoundaryWriteHandler3DTest.java  |  13 +-
 .../threed/obj/ObjFacetDefinitionReaderTest.java   |  11 +-
 .../threed/obj/ObjTriangleMeshReaderTest.java      |  11 +-
 .../io/euclidean/threed/obj/ObjWriterTest.java     |  50 ++---
 .../euclidean/threed/obj/PolygonObjParserTest.java |  91 ++++----
 .../stl/BinaryStlFacetDefinitionReaderTest.java    |  28 ++-
 .../euclidean/threed/stl/BinaryStlWriterTest.java  |  13 +-
 .../threed/stl/StlBoundaryReadHandler3DTest.java   |  21 +-
 .../threed/stl/StlBoundaryWriteHandler3DTest.java  |  45 +++-
 .../threed/stl/StlFacetDefinitionReadersTest.java  |   9 +-
 .../stl/TextStlFacetDefinitionReaderTest.java      |  19 +-
 .../io/euclidean/threed/stl/TextStlWriterTest.java |  47 ++--
 .../threed/txt/TextBoundaryReadHandler3DTest.java  |   9 +-
 .../threed/txt/TextBoundaryWriteHandler3DTest.java |  11 +-
 .../threed/txt/TextFacetDefinitionReaderTest.java  |  29 ++-
 .../threed/txt/TextFacetDefinitionWriterTest.java  |  29 ++-
 70 files changed, 1546 insertions(+), 1149 deletions(-)

diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryIOManager.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryIOManager.java
 index 5e1bb0c..3cba98b 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryIOManager.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryIOManager.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core;
 
-import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -39,8 +38,24 @@ import org.apache.commons.numbers.core.Precision;
  * All IO operations are delegated to registered format-specific {@link \
                BoundaryReadHandler read handlers}
  * and {@link BoundaryWriteHandler write handlers}.
  *
- * <p>Instances of this class are thread-safe as long as the registered handler \
                instances are
- * thread-safe.</p>
+ * <p><strong>Exceptions</strong>
+ * <p>Despite having functionality related to I/O operations, this class has been \
designed to <em>not</em> + * throw checked exceptions, in particular {@link \
java.io.IOException IOException}. The primary reasons for + * this choice are
+ * <ul>
+ *  <li>convenience,</li>
+ *  <li>compatibility with functional programming, and </li>
+ *  <li>the fact that modern Java practice is moving away from checked exceptions in \
general (as exemplified + *      by the JDK's {@link java.io.UncheckedIOException \
UncheckedIOException}).</li> + * </ul>
+ * As a result, any {@link java.io.IOException IOException} thrown internally by \
this or related classes + * is wrapped with {@link java.io.UncheckedIOException \
UncheckedIOException}. Other common runtime exceptions + * include {@link \
IllegalArgumentException}, which typically indicates mathematically invalid data, and \
+ * {@link IllegalStateException}, which typically indicates format or parsing \
errors. See the method-level + * documentation for more details.
+ *
+ * <p><strong>Implementation note:</strong> Instances of this class are thread-safe \
as long as the + * registered handler instances are thread-safe.</p>
  * @param <H> Geometric boundary type
  * @param <B> Boundary source type
  * @param <R> Read handler type
@@ -193,12 +208,12 @@ public class BoundaryIOManager<
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @param precision precision context used for floating point comparisons
      * @return object containing all boundaries from the input
-     * @throws IllegalArgumentException if no {@link BoundaryReadHandler read \
                handler}
-     *      can be found for the input format
-     * @throws IOException if an IO error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no +     *      {@link BoundaryReadHandler read handler} can be found \
for the input format +     * @throws IllegalStateException if a data format error \
occurs +     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public B read(final GeometryInput in, final GeometryFormat fmt, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public B read(final GeometryInput in, final GeometryFormat fmt, final \
Precision.DoubleEquivalence precision) {  return requireReadHandler(in, fmt).read(in, \
precision);  }
 
@@ -210,20 +225,24 @@ public class BoundaryIOManager<
      *      // access stream content
      *  }
      *  </pre>
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param in input to read boundaries from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to all boundaries from the input
-     * @throws IllegalArgumentException if no {@link BoundaryReadHandler read \
                handler}
-     *      can be found for the input format
-     * @throws IOException if an IO error occurs
+     * @throws IllegalArgumentException if no {@link BoundaryReadHandler read \
handler} can be found for +     *      the input format
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      */
     public Stream<H> boundaries(final GeometryInput in, final GeometryFormat fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return requireReadHandler(in, fmt).boundaries(in, precision);
     }
 
@@ -234,9 +253,9 @@ public class BoundaryIOManager<
      *      file extension of the output {@link GeometryOutput#getFileName()}
      * @throws IllegalArgumentException if no {@link BoundaryWriteHandler write \
                handler} can be found
      *      for the output format
-     * @throws IOException if an IO error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void write(final B src, final GeometryOutput out, final GeometryFormat \
fmt) throws IOException { +    public void write(final B src, final GeometryOutput \
out, final GeometryFormat fmt) {  requireWriteHandler(out, fmt).write(src, out);
     }
 
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryReadHandler.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryReadHandler.java
 index f3b489e..c753de0 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryReadHandler.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryReadHandler.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core;
 
-import java.io.IOException;
 import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
@@ -44,14 +43,15 @@ public interface BoundaryReadHandler<H extends \
HyperplaneConvexSubset<?>, B exte  GeometryFormat getFormat();
 
     /** Return an object containing all boundaries read from {@code input} using the \
                handler's
-     * supported data format. Implementations may throw runtime exceptions if \
                mathematically
-     * invalid boundaries are encountered.
-     * @param input input to read form
+     * supported data format.
+     * @param input input to read from
      * @param precision precision context used for floating point comparisons
      * @return object containing all boundaries read from {@code input}
-     * @throws IOException if an IO error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered +     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    B read(GeometryInput input, Precision.DoubleEquivalence precision) throws \
IOException; +    B read(GeometryInput input, Precision.DoubleEquivalence precision);
 
     /** Return a {@link Stream} that can be used to access all boundary information \
                from the given input,
      * which is expected to contain data in the format supported by this handler. \
Unlike the @@ -70,13 +70,17 @@ public interface BoundaryReadHandler<H extends \
                HyperplaneConvexSubset<?>, B exte
      *  }
      * </pre>
      *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param in input to read from
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to the boundary information from the given \
                input
-     * @throws IOException if an I/O error occurs during stream creation
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      */
-    Stream<H> boundaries(GeometryInput in, Precision.DoubleEquivalence precision) \
throws IOException; +    Stream<H> boundaries(GeometryInput in, \
Precision.DoubleEquivalence precision);  }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryWriteHandler.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryWriteHandler.java
 index a102ee1..9d996f0 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryWriteHandler.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/BoundaryWriteHandler.java
 @@ -16,8 +16,6 @@
  */
 package org.apache.commons.geometry.io.core;
 
-import java.io.IOException;
-
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.io.core.output.GeometryOutput;
@@ -45,7 +43,7 @@ public interface BoundaryWriteHandler<H extends \
                HyperplaneConvexSubset<?>, B ext
      * the instance.
      * @param src boundary source
      * @param out output to write to
-     * @throws IOException if an IO error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    void write(B src, GeometryOutput out) throws IOException;
+    void write(B src, GeometryOutput out);
 }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/FileGeometryInput.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/FileGeometryInput.java
 index ff751f6..2e9071a 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/FileGeometryInput.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/FileGeometryInput.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.core.input;
 
 import java.io.BufferedInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
@@ -62,8 +61,8 @@ public class FileGeometryInput extends AbstractGeometryInput {
      * <p>The returned input stream is buffered.</p>
      */
     @Override
-    public InputStream getInputStream() throws IOException {
-        return new BufferedInputStream(Files.newInputStream(file));
+    public InputStream getInputStream() {
+        return GeometryIOUtils.getUnchecked(() -> new \
BufferedInputStream(Files.newInputStream(file)));  }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/GeometryInput.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/GeometryInput.java
 index ba0bef3..92ad0f0 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/GeometryInput.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/GeometryInput.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core.input;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
 
@@ -38,7 +37,7 @@ public interface GeometryInput {
 
     /** Get the input stream for reading from the input.
      * @return input stream for reading from the input
-     * @throws IOException if an IO error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    InputStream getInputStream() throws IOException;
+    InputStream getInputStream();
 }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/UrlGeometryInput.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/UrlGeometryInput.java
 index e100064..15fba7a 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/UrlGeometryInput.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/input/UrlGeometryInput.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.core.input;
 
 import java.io.BufferedInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.charset.Charset;
@@ -61,8 +60,8 @@ public class UrlGeometryInput extends AbstractGeometryInput {
      * <p>The returned input stream is buffered.</p>
      */
     @Override
-    public InputStream getInputStream() throws IOException {
-        return new BufferedInputStream(url.openStream());
+    public InputStream getInputStream() {
+        return GeometryIOUtils.getUnchecked(() -> new \
BufferedInputStream(url.openStream()));  }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/CharReadBuffer.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/CharReadBuffer.java
 index c63adb5..72318e4 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/CharReadBuffer.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/CharReadBuffer.java
 @@ -22,7 +22,7 @@ import java.util.Objects;
 
 /** Class used to buffer characters read from an underlying {@link Reader}.
  * Characters can be consumed from the buffer, examined without being consumed,
- * and pushed back onto the buffer. The internal buffer is resized as needed.
+ * and pushed back onto the buffer. The internal bufer is resized as needed.
  */
 public class CharReadBuffer {
 
@@ -101,9 +101,9 @@ public class CharReadBuffer {
 
     /** Return true if more characters are available from the read buffer.
      * @return true if more characters are available from the read buffer
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public boolean hasMoreCharacters() throws IOException {
+    public boolean hasMoreCharacters() {
         return makeAvailable(1) > 0;
     }
 
@@ -112,9 +112,9 @@ public class CharReadBuffer {
      * is returned.
      * @param n number of characters requested to be available
      * @return number of characters available for immediate use in the buffer
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public int makeAvailable(final int n) throws IOException {
+    public int makeAvailable(final int n) {
         final int diff = n - count;
         if (diff > 0) {
             readChars(diff);
@@ -125,10 +125,10 @@ public class CharReadBuffer {
     /** Remove and return the next character in the buffer.
      * @return the next character in the buffer or {@value #EOF}
      *      if the end of the content has been reached
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #peek()
      */
-    public int read() throws IOException {
+    public int read() {
         final int result = peek();
         charsRemoved(1);
 
@@ -141,10 +141,10 @@ public class CharReadBuffer {
      * @param len requested length of the string
      * @return a string from the read buffer or null if no more characters are \
                available
      * @throws IllegalArgumentException if {@code len} is less than 0
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #peekString(int)
      */
-    public String readString(final int len) throws IOException {
+    public String readString(final int len) {
         final String result = peekString(len);
         if (result != null) {
             charsRemoved(result.length());
@@ -156,10 +156,10 @@ public class CharReadBuffer {
     /** Return the next character in the buffer without removing it.
      * @return the next character in the buffer or {@value #EOF}
      *      if the end of the content has been reached
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #read()
      */
-    public int peek() throws IOException {
+    public int peek() {
         if (makeAvailable(1) < 1) {
             return EOF;
         }
@@ -172,10 +172,10 @@ public class CharReadBuffer {
      * @param len requested length of the string
      * @return a string from the read buffer or null if no more characters are \
                available
      * @throws IllegalArgumentException if {@code len} is less than 0
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #readString(int)
      */
-    public String peekString(final int len) throws IOException {
+    public String peekString(final int len) {
         if (len < 0) {
             throw new IllegalArgumentException("Requested string length cannot be \
negative; was " + len);  } else if (len == 0) {
@@ -206,9 +206,9 @@ public class CharReadBuffer {
      * @param index index of the character to receive relative to the buffer start
      * @return the character at the given index of {@code -1} if the character is
      *      past the end of the stream content
-     * @throws IOException if an I/O exception occurs
+     * @throws java.io.UncheckedIOException if an I/O exception occurs
      */
-    public int charAt(final int index) throws IOException {
+    public int charAt(final int index) {
         if (index < 0) {
             throw new IllegalArgumentException("Character index cannot be negative; \
was " + index);  }
@@ -224,10 +224,10 @@ public class CharReadBuffer {
      * and then from the underlying reader using {@link Reader#skip(long)} if \
                needed.
      * @param n number of character to skip
      * @return the number of characters skipped
-     * @throws IOException if an I/O error occurs
      * @throws IllegalArgumentException if {@code n} is negative
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public int skip(final int n) throws IOException {
+    public int skip(final int n) {
         if (n < 0) {
             throw new IllegalArgumentException("Character skip count cannot be \
negative; was " + n);  }
@@ -239,7 +239,11 @@ public class CharReadBuffer {
         // skip from the reader if required
         final int remaining = n - skipped;
         if (remaining > 0) {
-            skipped += reader.skip(remaining);
+            try {
+                skipped += reader.skip(remaining);
+            } catch (IOException exc) {
+                throw GeometryIOUtils.createUnchecked(exc);
+            }
         }
 
         return skipped;
@@ -281,29 +285,33 @@ public class CharReadBuffer {
      * the internal buffer.
      * @param n minimum number of characters requested to be placed
      *      in the buffer
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void readChars(final int n) throws IOException {
+    private void readChars(final int n) {
         if (!reachedEof) {
             int remaining = Math.max(n, minRead);
 
             ensureCapacity(count + remaining);
 
-            int tail;
-            int len;
-            int read;
-            while (remaining > 0) {
-                tail = (head + count) % buffer.length;
-                len = Math.min(buffer.length - tail, remaining);
-
-                read = reader.read(buffer, tail, len);
-                if (read == EOF) {
-                    reachedEof = true;
-                    break;
+            try {
+                int tail;
+                int len;
+                int read;
+                while (remaining > 0) {
+                    tail = (head + count) % buffer.length;
+                    len = Math.min(buffer.length - tail, remaining);
+
+                    read = reader.read(buffer, tail, len);
+                    if (read == EOF) {
+                        reachedEof = true;
+                        break;
+                    }
+
+                    charsAppended(read);
+                    remaining -= read;
                 }
-
-                charsAppended(read);
-                remaining -= read;
+            } catch (IOException exc) {
+                throw GeometryIOUtils.createUnchecked(exc);
             }
         }
     }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtils.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtils.java
 index 64beb23..63bd9d7 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtils.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtils.java
 @@ -16,15 +16,23 @@
  */
 package org.apache.commons.geometry.io.core.internal;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.UncheckedIOException;
 import java.net.URL;
+import java.nio.charset.Charset;
 import java.nio.file.Path;
 import java.util.stream.Stream;
 
-/** Class containing utility methods for IO operations.
+import org.apache.commons.geometry.io.core.input.GeometryInput;
+import org.apache.commons.geometry.io.core.output.GeometryOutput;
+
+/** Internal class containing utility methods for IO operations.
  */
 public final class GeometryIOUtils {
 
@@ -103,6 +111,94 @@ public final class GeometryIOUtils {
         return null;
     }
 
+    /** Create a {@link BufferedReader} for reading from the given input. The \
charset used is the charset +     * defined in {@code input} or {@code \
defaultCharset} if null. +     * @param input input to read from
+     * @param defaultCharset charset to use if no charset is defined in the input
+     * @return new reader instance
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public static BufferedReader createBufferedReader(final GeometryInput input, \
final Charset defaultCharset) { +        final Charset charset = input.getCharset() \
!= null ? +                input.getCharset() :
+                defaultCharset;
+
+        return new BufferedReader(new InputStreamReader(input.getInputStream(), \
charset)); +    }
+
+    /** Create a {@link BufferedWriter} for writing to the given output. The charset \
used is the charset +     * defined in {@code output} or {@code defaultCharset} if \
null. +     * @param output output to write to
+     * @param defaultCharset charset to use if no charset is defined in the output
+     * @return new writer instance
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public static BufferedWriter createBufferedWriter(final GeometryOutput output, \
final Charset defaultCharset) { +        final Charset charset = output.getCharset() \
!= null ? +                output.getCharset() :
+                defaultCharset;
+
+        return new BufferedWriter(new OutputStreamWriter(output.getOutputStream(), \
charset)); +    }
+
+    /** Get a value from {@code supplier}, wrapping any {@link IOException} with
+     * {@link UncheckedIOException}.
+     * @param <T> returned type
+     * @param supplier object supplying the return value
+     * @return supplied value
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public static <T> T getUnchecked(final IOSupplier<T> supplier) {
+        try {
+            return supplier.get();
+        } catch (IOException exc) {
+            throw createUnchecked(exc);
+        }
+    }
+
+    /** Pass the given argument to the consumer, wrapping any {@link IOException} \
with +     * {@link UncheckedIOException}.
+     * @param <T> argument type
+     * @param consumer function to call
+     * @param arg function argument
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public static <T> void acceptUnchecked(final IOConsumer<T> consumer, final T \
arg) { +        try {
+            consumer.accept(arg);
+        } catch (IOException exc) {
+            throw createUnchecked(exc);
+        }
+    }
+
+    /** Call the given function with the argument and return the {@code int} result, \
wrapping any +     * {@link IOException} with {@link UncheckedIOException}.
+     * @param <T> argument type
+     * @param fn function to call
+     * @param arg function argument
+     * @return int value
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public static <T> int applyAsIntUnchecked(final IOToIntFunction<T> fn, final T \
arg) { +        try {
+            return fn.applyAsInt(arg);
+        } catch (IOException exc) {
+            throw createUnchecked(exc);
+        }
+    }
+
+    /** Close the argument, wrapping any IO exceptions with {@link \
UncheckedIOException}. +     * @param closeable argument to close
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public static void closeUnchecked(final Closeable closeable) {
+        try {
+            closeable.close();
+        } catch (IOException exc) {
+            throw createUnchecked(exc);
+        }
+    }
+
     /** Create an unchecked exception from the given checked exception. The message \
                of the
      * returned exception contains the original exception's type and message.
      * @param exc exception to wrap in an unchecked exception
@@ -113,6 +209,23 @@ public final class GeometryIOUtils {
         return new UncheckedIOException(msg, exc);
     }
 
+    /** Create an exception indicating a parsing or syntax error.
+     * @param msg exception message
+     * @return an exception indicating a parsing or syntax error
+     */
+    public static IllegalStateException parseError(final String msg) {
+        return parseError(msg, null);
+    }
+
+    /** Create an exception indicating a parsing or syntax error.
+     * @param msg exception message
+     * @param cause exception cause
+     * @return an exception indicating a parsing or syntax error
+     */
+    public static IllegalStateException parseError(final String msg, final Throwable \
cause) { +        return new IllegalStateException(msg, cause);
+    }
+
     /** Pass a supplied {@link Closeable} instance to {@code function} and return \
                the result.
      * The {@code Closeable} instance returned by the supplier is closed if function \
                execution
      * fails, otherwise the instance is <em>not</em> closed.
@@ -121,25 +234,30 @@ public final class GeometryIOUtils {
      * @param function function called with the supplied Closeable instance
      * @param closeableSupplier supplier used to obtain a Closeable instance
      * @return result of calling {@code function} with a supplied Closeable instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public static <T, C extends Closeable> T tryApplyCloseable(final IOFunction<C, \
                T> function,
-            final IOSupplier<? extends C> closeableSupplier) throws IOException {
+            final IOSupplier<? extends C> closeableSupplier) {
         C closeable = null;
+        RuntimeException exc;
         try {
             closeable = closeableSupplier.get();
             return function.apply(closeable);
-        } catch (IOException | RuntimeException exc) {
-            if (closeable != null) {
-                try {
-                    closeable.close();
-                } catch (IOException suppressed) {
-                    exc.addSuppressed(suppressed);
-                }
-            }
+        } catch (RuntimeException e) {
+            exc = e;
+        } catch (IOException e) {
+            exc = createUnchecked(e);
+        }
 
-            throw exc;
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (IOException suppressed) {
+                exc.addSuppressed(suppressed);
+            }
         }
+
+        throw exc;
     }
 
     /** Create a stream associated with an input stream. The input stream is closed \
when the @@ -150,11 +268,10 @@ public final class GeometryIOUtils {
      * @param streamFunction function accepting an input stream and returning a \
                stream
      * @param inputStreamSupplier supplier used to obtain the input stream
      * @return stream associated with the input stream return by the supplier
-     * @throws IOException if an I/O error occurs during input stream and stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                input stream and stream creation
      */
     public static <T, I extends InputStream> Stream<T> createCloseableStream(
-            final IOFunction<I, Stream<T>> streamFunction, final IOSupplier<? \
                extends I> inputStreamSupplier)
-                throws IOException {
+            final IOFunction<I, Stream<T>> streamFunction, final IOSupplier<? \
extends I> inputStreamSupplier) {  return tryApplyCloseable(
                 in -> \
streamFunction.apply(in).onClose(closeAsUncheckedRunnable(in)),  \
inputStreamSupplier); @@ -166,12 +283,6 @@ public final class GeometryIOUtils {
      * @return runnable that calls {@code close()) on the argument
      */
     private static Runnable closeAsUncheckedRunnable(final Closeable closeable) {
-        return () -> {
-            try {
-                closeable.close();
-            } catch (IOException exc) {
-                throw createUnchecked(exc);
-            }
-        };
+        return () -> closeUnchecked(closeable);
     }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/IOConsumer.java
 similarity index 59%
copy from commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
 copy to commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/IOConsumer.java
 index be338b6..39a4efd 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/IOConsumer.java
 @@ -14,21 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.io.euclidean.threed;
+package org.apache.commons.geometry.io.core.internal;
 
-import java.io.Closeable;
 import java.io.IOException;
 
-/** Interface for reading {@link FacetDefinition facet definitions} from an input \
                source.
- * @see FacetDefinition
+/** Functional interface similar to {@link java.util.function.Consumer} but allowing \
an + * {@code IOException} to be thrown.
+ * @param <T> Type accepted by the function
  */
-public interface FacetDefinitionReader extends Closeable {
+@FunctionalInterface
+public interface IOConsumer<T> {
 
-    /** Return the next facet definition from the input source or null if no more
-     * facets are available.
-     * @return the next facet definition or null if no more facets
-     *      are available
-     * @throws IOException if an I/O or data format error occurs
+    /** Perform an operation with the given argument.
+     * @param value argument
+     * @throws IOException if an I/O error occurs
      */
-    FacetDefinition readFacet() throws IOException;
+    void accept(T value) throws IOException;
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/IOToIntFunction.java
 similarity index 59%
copy from commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
 copy to commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/IOToIntFunction.java
 index be338b6..50f6756 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/IOToIntFunction.java
 @@ -14,21 +14,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.io.euclidean.threed;
+package org.apache.commons.geometry.io.core.internal;
 
-import java.io.Closeable;
 import java.io.IOException;
 
-/** Interface for reading {@link FacetDefinition facet definitions} from an input \
                source.
- * @see FacetDefinition
+/** Functional interface similar to {@link java.util.function.ToIntFunction} but \
allowing an + * {@code IOException} to be thrown.
+ * @param <T> Input type
  */
-public interface FacetDefinitionReader extends Closeable {
+@FunctionalInterface
+public interface IOToIntFunction<T> {
 
-    /** Return the next facet definition from the input source or null if no more
-     * facets are available.
-     * @return the next facet definition or null if no more facets
-     *      are available
-     * @throws IOException if an I/O or data format error occurs
+    /** Apply this function to the argument.
+     * @param value argument
+     * @return int value
+     * @throws IOException if an I/O error occurs
      */
-    FacetDefinition readFacet() throws IOException;
+    int applyAsInt(T value) throws IOException;
 }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/SimpleTextParser.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/SimpleTextParser.java
 index 4ff7f6b..dc60ff5 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/SimpleTextParser.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/internal/SimpleTextParser.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core.internal;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.util.Arrays;
 import java.util.List;
@@ -185,10 +184,10 @@ public class SimpleTextParser {
 
     /** Get the current token parsed as an integer.
      * @return the current token parsed as an integer
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if the current token cannot be parsed as an integer
+     * @throws IllegalStateException if no token has been read or the
+     *      current token cannot be parsed as an integer
      */
-    public int getCurrentTokenAsInt() throws IOException {
+    public int getCurrentTokenAsInt() {
         ensureHasSetToken();
 
         Throwable cause = null;
@@ -206,10 +205,10 @@ public class SimpleTextParser {
 
     /** Get the current token parsed as a double.
      * @return the current token parsed as a double
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if the current token cannot be parsed as a double
+     * @throws IllegalStateException if no token has been read or the
+     *      current token cannot be parsed as a double
      */
-    public double getCurrentTokenAsDouble() throws IOException {
+    public double getCurrentTokenAsDouble() {
         ensureHasSetToken();
 
         Throwable cause = null;
@@ -227,17 +226,17 @@ public class SimpleTextParser {
 
     /** Return true if there are more characters to read from this instance.
      * @return true if there are more characters to read from this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public boolean hasMoreCharacters() throws IOException {
+    public boolean hasMoreCharacters() {
         return buffer.hasMoreCharacters();
     }
 
     /** Return true if there are more characters to read on the current line.
      * @return true if there are more characters to read on the current line
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public boolean hasMoreCharactersOnLine() throws IOException {
+    public boolean hasMoreCharactersOnLine() {
         return hasMoreCharacters() && isNotNewLinePart(peekChar());
     }
 
@@ -246,10 +245,10 @@ public class SimpleTextParser {
      * set the {@link #getCurrentToken() current token}.
      * @return the next character in the stream or -1 if the end of the stream has \
                been
      *      reached
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #peekChar()
      */
-    public int readChar() throws IOException {
+    public int readChar() {
         final int value = buffer.read();
         if (value == LF ||
                 (value == CR && peekChar() != LF)) {
@@ -271,11 +270,11 @@ public class SimpleTextParser {
      * @return this instance
      * @throws IllegalArgumentException if {@code len} is less than 0 or greater \
                than the
      *      configured {@link #getMaxStringLength() maximum string length}
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      * @see #consume(int, IntConsumer)
      */
-    public SimpleTextParser next(final int len) throws IOException {
+    public SimpleTextParser next(final int len) {
         validateRequestedStringLength(len);
 
         final int line = getLineNumber();
@@ -303,12 +302,11 @@ public class SimpleTextParser {
      * @return this instance
      * @throws IllegalArgumentException if {@code len} is less than 0 or greater \
                than the
      *      configured {@link #getMaxStringLength() maximum string length}
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      * @see #consumeWithLineContinuation(char, int, IntConsumer)
      */
-    public SimpleTextParser nextWithLineContinuation(final char \
                lineContinuationChar, final int len)
-            throws IOException {
+    public SimpleTextParser nextWithLineContinuation(final char \
lineContinuationChar, final int len) {  validateRequestedStringLength(len);
 
         final int line = getLineNumber();
@@ -336,12 +334,13 @@ public class SimpleTextParser {
      * @param pred predicate function passed characters read from the input; reading \
                continues
      *      until the predicate returns false
      * @return this instance
-     * @throws IOException if an I/O error occurs or the length of the produced \
string exceeds the configured +     * @throws IllegalStateException if the length of \
                the produced string exceeds the configured
      *      {@link #getMaxStringLength() maximum string length}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      * @see #consume(IntPredicate, IntConsumer)
      */
-    public SimpleTextParser next(final IntPredicate pred) throws IOException {
+    public SimpleTextParser next(final IntPredicate pred) {
         final int line = getLineNumber();
         final int col = getColumnNumber();
 
@@ -366,13 +365,13 @@ public class SimpleTextParser {
      * @param pred predicate function passed characters read from the input; reading \
                continues
      *      until the predicate returns false
      * @return this instance
-     * @throws IOException if an I/O error occurs or the length of the produced \
string exceeds the configured +     * @throws IllegalStateException if the length of \
                the produced string exceeds the configured
      *      {@link #getMaxStringLength() maximum string length}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      * @see #consume(IntPredicate, IntConsumer)
      */
-    public SimpleTextParser nextWithLineContinuation(final char \
                lineContinuationChar, final IntPredicate pred)
-            throws IOException {
+    public SimpleTextParser nextWithLineContinuation(final char \
lineContinuationChar, final IntPredicate pred) {  final int line = getLineNumber();
         final int col = getColumnNumber();
 
@@ -395,11 +394,12 @@ public class SimpleTextParser {
      * ('\r', '\n', or '\r\n') at the end of the line is consumed but is not \
                included in the token.
      * The token will be null if the end of the stream has been reached prior to the \
                method call.
      * @return this instance
-     * @throws IOException if an I/O error occurs or the length of the produced \
string exceeds the configured +     * @throws IllegalStateException if the length of \
                the produced string exceeds the configured
      *      {@link #getMaxStringLength() maximum string length}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      */
-    public SimpleTextParser nextLine() throws IOException {
+    public SimpleTextParser nextLine() {
         next(SimpleTextParser::isNotNewLinePart);
 
         discardNewLineSequence();
@@ -412,11 +412,12 @@ public class SimpleTextParser {
      * character in the stream is not alphanumeric and will be null if the end of \
                the stream has
      * been reached prior to the method call.
      * @return this instance
-     * @throws IOException if an I/O error occurs or the length of the produced \
string exceeds the configured +     * @throws IllegalStateException if the length of \
                the produced string exceeds the configured
      *      {@link #getMaxStringLength() maximum string length}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      */
-    public SimpleTextParser nextAlphanumeric() throws IOException {
+    public SimpleTextParser nextAlphanumeric() {
         return next(SimpleTextParser::isAlphanumeric);
     }
 
@@ -424,9 +425,9 @@ public class SimpleTextParser {
      * parser position is updated but the current token is not changed.
      * @param len number of characters to discard
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser discard(final int len) throws IOException {
+    public SimpleTextParser discard(final int len) {
         return consume(len, NOOP_CONSUMER);
     }
 
@@ -436,10 +437,10 @@ public class SimpleTextParser {
      * @param lineContinuationChar character used to indicate skipped new line \
                sequences
      * @param len number of characters to discard
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public SimpleTextParser discardWithLineContinuation(final char \
                lineContinuationChar,
-            final int len) throws IOException {
+            final int len) {
         return consumeWithLineContinuation(lineContinuationChar, len, \
NOOP_CONSUMER);  }
 
@@ -449,9 +450,9 @@ public class SimpleTextParser {
      * token is not changed.
      * @param pred predicate test for characters to discard
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser discard(final IntPredicate pred) throws IOException {
+    public SimpleTextParser discard(final IntPredicate pred) {
         return consume(pred, NOOP_CONSUMER);
     }
 
@@ -462,10 +463,10 @@ public class SimpleTextParser {
      * @param lineContinuationChar character used to indicate skipped new line \
                sequences
      * @param pred predicate test for characters to discard
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public SimpleTextParser discardWithLineContinuation(final char \
                lineContinuationChar,
-            final IntPredicate pred) throws IOException {
+            final IntPredicate pred) {
         return consumeWithLineContinuation(lineContinuationChar, pred, \
NOOP_CONSUMER);  }
 
@@ -474,9 +475,9 @@ public class SimpleTextParser {
      * character or -1 if the end of the stream has been reached. The parser \
                position is updated
      * but the current token is not changed.
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser discardWhitespace() throws IOException {
+    public SimpleTextParser discardWhitespace() {
         return discard(SimpleTextParser::isWhitespace);
     }
 
@@ -485,9 +486,9 @@ public class SimpleTextParser {
      * the newline character sequence (indicating the end of the line), or -1 \
                (indicating the
      * end of the stream). The parser position is updated but the current token is \
                not changed.
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser discardLineWhitespace() throws IOException {
+    public SimpleTextParser discardLineWhitespace() {
         return discard(SimpleTextParser::isLineWhitespace);
     }
 
@@ -495,9 +496,9 @@ public class SimpleTextParser {
      * is defined as one of "\r", "\n", or "\r\n". Does nothing if the reader is not \
                positioned
      * at a newline sequence. The parser position is updated but the current token \
                is not changed.
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser discardNewLineSequence() throws IOException {
+    public SimpleTextParser discardNewLineSequence() {
         final int value = peekChar();
         if (value == LF) {
             readChar();
@@ -517,9 +518,9 @@ public class SimpleTextParser {
      * first character on the next line or -1 if the end of the stream has been \
                reached.
      * The parser position is updated but the current token is not changed.
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser discardLine() throws IOException {
+    public SimpleTextParser discardLine() {
         discard(SimpleTextParser::isNotNewLinePart);
 
         discardNewLineSequence();
@@ -533,9 +534,9 @@ public class SimpleTextParser {
      * @param pred predicate test for characters to consume
      * @param consumer object to be passed each consumed character
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser consume(final IntPredicate pred, final IntConsumer \
consumer) throws IOException { +    public SimpleTextParser consume(final \
IntPredicate pred, final IntConsumer consumer) {  int ch;
         while ((ch = peekChar()) != EOF && pred.test(ch)) {
             consumer.accept(readChar());
@@ -551,10 +552,10 @@ public class SimpleTextParser {
      * @param len number of characters to consume
      * @param consumer function to be passed each consumed character
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public SimpleTextParser consumeWithLineContinuation(final char \
                lineContinuationChar,
-            final int len, final IntConsumer consumer) throws IOException {
+            final int len, final IntConsumer consumer) {
         int i = -1;
         int ch;
         while (++i < len && (ch = readChar()) != EOF) {
@@ -575,9 +576,9 @@ public class SimpleTextParser {
      * @param len number of characters to consume
      * @param consumer object to be passed each consumed character
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public SimpleTextParser consume(final int len, final IntConsumer consumer) \
throws IOException { +    public SimpleTextParser consume(final int len, final \
IntConsumer consumer) {  int ch;
         for (int i = 0; i < len; ++i) {
             ch = readChar();
@@ -598,10 +599,10 @@ public class SimpleTextParser {
      * @param pred predicate test for characters to consume
      * @param consumer object to be passed each consumed character
      * @return this instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public SimpleTextParser consumeWithLineContinuation(final char \
                lineContinuationChar,
-            final IntPredicate pred, final IntConsumer consumer) throws IOException \
{ +            final IntPredicate pred, final IntConsumer consumer) {
         int ch;
         while ((ch = peekChar()) != EOF) {
             if (ch == lineContinuationChar && isNewLinePart(buffer.charAt(1))) {
@@ -620,10 +621,10 @@ public class SimpleTextParser {
     /** Return the next character in the stream but do not advance the parser \
                position.
      * @return the next character in the stream or -1 if the end of the stream has \
                been
      *      reached
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #readChar()
      */
-    public int peekChar() throws IOException {
+    public int peekChar() {
         return buffer.peek();
     }
 
@@ -635,10 +636,10 @@ public class SimpleTextParser {
      *      or null if the parser has already reached the end of the stream
      * @throws IllegalArgumentException if {@code len} is less than 0 or greater \
                than the
      *      configured {@link #getMaxStringLength() maximum string length}
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #next(int)
      */
-    public String peek(final int len) throws IOException {
+    public String peek(final int len) {
         validateRequestedStringLength(len);
 
         return buffer.peekString(len);
@@ -650,11 +651,12 @@ public class SimpleTextParser {
      *      until the predicate returns false
      * @return string containing characters matching {@code pred} or null if the \
                parser has already
      *      reached the end of the stream
-     * @throws IOException if an I/O error occurs or the length of the produced \
string exceeds the configured +     * @throws IllegalStateException if the length of \
                the produced string exceeds the configured
      *      {@link #getMaxStringLength() maximum string length}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getCurrentToken()
      */
-    public String peek(final IntPredicate pred) throws IOException {
+    public String peek(final IntPredicate pred) {
         String token = null;
 
         if (hasMoreCharacters()) {
@@ -678,10 +680,10 @@ public class SimpleTextParser {
      * exception if they are not equal. The comparison is case-sensitive.
      * @param expected expected token
      * @return this instance
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if {@code expected} does not exactly equal the current \
token +     * @throws IllegalStateException if no token has been read or {@code \
expected} does not exactly +     *      equal the current token
      */
-    public SimpleTextParser match(final String expected) throws IOException {
+    public SimpleTextParser match(final String expected) {
         matchInternal(expected, true, true);
         return this;
     }
@@ -690,10 +692,10 @@ public class SimpleTextParser {
      * exception if they are not equal. The comparison is <em>not</em> \
                case-sensitive.
      * @param expected expected token
      * @return this instance
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if {@code expected} does not equal the current token \
(ignoring case) +     * @throws IllegalStateException if no token has been read or \
{@code expected} does not equal +     *      the current token (ignoring case)
      */
-    public SimpleTextParser matchIgnoreCase(final String expected) throws \
IOException { +    public SimpleTextParser matchIgnoreCase(final String expected) {
         matchInternal(expected, false, true);
         return this;
     }
@@ -703,9 +705,9 @@ public class SimpleTextParser {
      * @param expected expected token
      * @return true if the argument exactly equals the current token
      * @throws IllegalStateException if no token has been read
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public boolean tryMatch(final String expected) throws IOException {
+    public boolean tryMatch(final String expected) {
         return matchInternal(expected, true, false);
     }
 
@@ -714,9 +716,8 @@ public class SimpleTextParser {
      * @param expected expected token
      * @return true if the argument equals the current token (ignoring case)
      * @throws IllegalStateException if no token has been read
-     * @throws IOException if an I/O error occurs
      */
-    public boolean tryMatchIgnoreCase(final String expected) throws IOException {
+    public boolean tryMatchIgnoreCase(final String expected) {
         return matchInternal(expected, false, false);
     }
 
@@ -726,12 +727,11 @@ public class SimpleTextParser {
      * @param throwOnFailure if an exception should be thrown if the argument is not
      *      equal to the current token
      * @return true if the argument is equal to the current token
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if {@code expected} does not match the current token and
-     *      {@code throwOnFailure} is true
+     * @throws IllegalStateException if no token has been read or {@code expected} \
does not match the +     *      current token and {@code throwOnFailure} is true
      */
     private boolean matchInternal(final String expected, final boolean \
                caseSensitive,
-            final boolean throwOnFailure) throws IOException {
+            final boolean throwOnFailure) {
         ensureHasSetToken();
 
         if (!stringsEqual(expected, currentToken, caseSensitive)) {
@@ -749,10 +749,9 @@ public class SimpleTextParser {
      * An exception is thrown if no match is found. String comparisons are \
                case-sensitive.
      * @param expected strings to compare with the current token
      * @return index of the argument that exactly matches the current token
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if no match is found among the arguments
+     * @throws IllegalStateException if no token has been read or no match is found \
                among the arguments
      */
-    public int choose(final String... expected) throws IOException {
+    public int choose(final String... expected) {
         return choose(Arrays.asList(expected));
     }
 
@@ -760,10 +759,9 @@ public class SimpleTextParser {
      * An exception is thrown if no match is found. String comparisons are \
                case-sensitive.
      * @param expected strings to compare with the current token
      * @return index of the argument that exactly matches the current token
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if no match is found among the arguments
+     * @throws IllegalStateException if no token has been read or no match is found \
                among the arguments
      */
-    public int choose(final List<String> expected) throws IOException {
+    public int choose(final List<String> expected) {
         return chooseInternal(expected, true, true);
     }
 
@@ -772,10 +770,9 @@ public class SimpleTextParser {
      * case-sensitive.
      * @param expected strings to compare with the current token
      * @return index of the argument that matches the current token (ignoring case)
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if no match is found among the arguments
+     * @throws IllegalStateException if no token has been read or no match is found \
                among the arguments
      */
-    public int chooseIgnoreCase(final String... expected) throws IOException {
+    public int chooseIgnoreCase(final String... expected) {
         return chooseIgnoreCase(Arrays.asList(expected));
     }
 
@@ -784,10 +781,9 @@ public class SimpleTextParser {
      * case-sensitive.
      * @param expected strings to compare with the current token
      * @return index of the argument that matches the current token (ignoring case)
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if no match is found among the arguments
+     * @throws IllegalStateException if no token has been read or no match is found \
                among the arguments
      */
-    public int chooseIgnoreCase(final List<String> expected) throws IOException {
+    public int chooseIgnoreCase(final List<String> expected) {
         return chooseInternal(expected, false, true);
     }
 
@@ -797,9 +793,8 @@ public class SimpleTextParser {
      * @return index of the argument that exactly matches the current token or -1 if
      *      no match is found
      * @throws IllegalStateException if no token has been read
-     * @throws IOException if an I/O error occurs
      */
-    public int tryChoose(final String... expected) throws IOException {
+    public int tryChoose(final String... expected) {
         return tryChoose(Arrays.asList(expected));
     }
 
@@ -809,9 +804,8 @@ public class SimpleTextParser {
      * @return index of the argument that exactly matches the current token or -1 if
      *      no match is found
      * @throws IllegalStateException if no token has been read
-     * @throws IOException if an I/O error occurs
      */
-    public int tryChoose(final List<String> expected) throws IOException {
+    public int tryChoose(final List<String> expected) {
         return chooseInternal(expected, true, false);
     }
 
@@ -821,9 +815,8 @@ public class SimpleTextParser {
      * @return index of the argument that matches the current token (ignoring case) \
                or -1 if
      *      no match is found
      * @throws IllegalStateException if no token has been read
-     * @throws IOException if an I/O error occurs
      */
-    public int tryChooseIgnoreCase(final String... expected) throws IOException {
+    public int tryChooseIgnoreCase(final String... expected) {
         return tryChooseIgnoreCase(Arrays.asList(expected));
     }
 
@@ -833,9 +826,8 @@ public class SimpleTextParser {
      * @return index of the argument that matches the current token (ignoring case) \
                or -1 if
      *      no match is found
      * @throws IllegalStateException if no token has been read
-     * @throws IOException is an I/O error occurs
      */
-    public int tryChooseIgnoreCase(final List<String> expected) throws IOException {
+    public int tryChooseIgnoreCase(final List<String> expected) {
         return chooseInternal(expected, false, false);
     }
 
@@ -845,11 +837,11 @@ public class SimpleTextParser {
      * @param caseSensitive if the comparisons should be case-sensitive
      * @param throwOnFailure if an exception should be thrown if no match is found
      * @return the index of the matching argument or -1 if no match is found
-     * @throws IllegalStateException if no token has been read
-     * @throws IOException if no match is found and {@code throwOnFailure} is true
+     * @throws IllegalStateException if no token has been read or no match is found \
and +     *      {@code throwOnFailure} is true
      */
     private int chooseInternal(final List<String> expected, final boolean \
                caseSensitive,
-            final boolean throwOnFailure) throws IOException {
+            final boolean throwOnFailure) {
         ensureHasSetToken();
 
         int i = 0;
@@ -874,7 +866,7 @@ public class SimpleTextParser {
      * @param expected string describing what was expected
      * @return exception indicating that the current token was unexpected
      */
-    public IOException unexpectedToken(final String expected) {
+    public IllegalStateException unexpectedToken(final String expected) {
         return unexpectedToken(expected, null);
     }
 
@@ -885,7 +877,7 @@ public class SimpleTextParser {
      * @param cause cause of the error
      * @return exception indicating that the current token was unexpected
      */
-    public IOException unexpectedToken(final String expected, final Throwable cause) \
{ +    public IllegalStateException unexpectedToken(final String expected, final \
Throwable cause) {  
         StringBuilder msg = new StringBuilder();
         msg.append("expected ")
@@ -903,7 +895,7 @@ public class SimpleTextParser {
      * @param msg error message
      * @return an exception indicating an error during parsing at the current token \
                position
      */
-    public IOException tokenError(final String msg) {
+    public IllegalStateException tokenError(final String msg) {
         return tokenError(msg, null);
     }
 
@@ -912,7 +904,7 @@ public class SimpleTextParser {
      * @param cause the cause of the error; may be null
      * @return an exception indicating an error during parsing at the current token \
                position
      */
-    public IOException tokenError(final String msg, final Throwable cause) {
+    public IllegalStateException tokenError(final String msg, final Throwable cause) \
                {
         final int line = hasSetToken ? currentTokenLineNumber : lineNumber;
         final int col = hasSetToken ? currentTokenColumnNumber : columnNumber;
 
@@ -923,7 +915,7 @@ public class SimpleTextParser {
      * @param msg error message
      * @return an exception indicating an error during parsing
      */
-    public IOException parseError(final String msg) {
+    public IllegalStateException parseError(final String msg) {
         return parseError(msg, null);
     }
 
@@ -932,7 +924,7 @@ public class SimpleTextParser {
      * @param cause the cause of the error; may be null
      * @return an exception indicating an error during parsing
      */
-    public IOException parseError(final String msg, final Throwable cause) {
+    public IllegalStateException parseError(final String msg, final Throwable cause) \
{  return parseError(lineNumber, columnNumber, msg, cause);
     }
 
@@ -942,7 +934,7 @@ public class SimpleTextParser {
      * @param msg error message
      * @return an exception indicating an error during parsing
      */
-    public IOException parseError(final int line, final int col, final String msg) {
+    public IllegalStateException parseError(final int line, final int col, final \
String msg) {  return parseError(line, col, msg, null);
     }
 
@@ -953,21 +945,11 @@ public class SimpleTextParser {
      * @param cause the cause of the error
      * @return an exception indicating an error during parsing
      */
-    public IOException parseError(final int line, final int col, final String msg,
+    public IllegalStateException parseError(final int line, final int col, final \
String msg,  final Throwable cause) {
         final String fullMsg = String.format("Parsing failed at line %d, column %d: \
%s",  line, col, msg);
-        return createParseError(fullMsg, cause);
-    }
-
-    /** Construct a new parse exception instance with the given message and cause. \
                Subclasses
-     *  may override this method to provide their own exception types.
-     * @param msg error message
-     * @param cause error cause
-     * @return a new parse exception instance
-     */
-    protected IOException createParseError(final String msg, final Throwable cause) \
                {
-        return new ParseException(msg, cause);
+        return GeometryIOUtils.parseError(fullMsg, cause);
     }
 
     /** Set the current token string and position.
@@ -1001,7 +983,7 @@ public class SimpleTextParser {
                         return "empty token followed by [" + peek(1) + "]";
                     }
                 }
-            } catch (IOException exc) {
+            } catch (IllegalStateException exc) {
                 // ignore
             }
         }
@@ -1202,9 +1184,9 @@ public class SimpleTextParser {
 
         /** Get the string collected by this instance.
          * @return the string collected by this instance
-         * @throws IOException if the string exceeds the maximum configured length
+         * @throws IllegalStateException if the string exceeds the maximum \
                configured length
          */
-        public String getString() throws IOException {
+        public String getString() {
             if (hasExceededMaxStringLength()) {
                 throw parseError(line, col, STRING_LENGTH_ERR_MSG + \
maxStringLength);  }
@@ -1219,19 +1201,4 @@ public class SimpleTextParser {
             return sb.length() > maxStringLength;
         }
     }
-
-    /** Exception used to indicate a parsing error. */
-    private static final class ParseException extends IOException {
-
-        /** Serializable UID. */
-        private static final long serialVersionUID = 20210113L;
-
-        /** Construct a new instance with the given message and cause.
-         * @param msg exception message
-         * @param cause exception cause; may be null
-         */
-        ParseException(final String msg, final Throwable cause) {
-            super(msg, cause);
-        }
-    }
 }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/FileGeometryOutput.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/FileGeometryOutput.java
 index d51cbfc..5ea9531 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/FileGeometryOutput.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/FileGeometryOutput.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.core.output;
 
 import java.io.BufferedOutputStream;
-import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
@@ -61,8 +60,8 @@ public class FileGeometryOutput extends AbstractGeometryOutput {
      * <p>The returned output stream is buffered.</p>
      */
     @Override
-    public OutputStream getOutputStream() throws IOException {
-        return new BufferedOutputStream(Files.newOutputStream(file));
+    public OutputStream getOutputStream() {
+        return GeometryIOUtils.getUnchecked(() -> new \
BufferedOutputStream(Files.newOutputStream(file)));  }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/GeometryOutput.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/GeometryOutput.java
 index d755445..99f70e3 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/GeometryOutput.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/output/GeometryOutput.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core.output;
 
-import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
 
@@ -38,7 +37,7 @@ public interface GeometryOutput {
 
     /** Get the output stream for writing to the output.
      * @return output stream for writing to the output
-     * @throws IOException if an IO error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    OutputStream getOutputStream() throws IOException;
+    OutputStream getOutputStream();
 }
diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriter.java \
b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriter.java
 index f4eb918..180f536 100644
--- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriter.java
                
+++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriter.java
 @@ -21,6 +21,8 @@ import java.io.IOException;
 import java.io.Writer;
 import java.util.function.DoubleFunction;
 
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
+
 /** Base type for classes that write text-based data formats. This class
  * provides a number of common configuration options and utility methods.
  */
@@ -85,8 +87,8 @@ public abstract class AbstractTextFormatWriter implements Closeable \
{  
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
-        writer.close();
+    public void close() {
+        GeometryIOUtils.closeUnchecked(writer);
     }
 
     /** Get the underlying writer instance.
@@ -98,40 +100,48 @@ public abstract class AbstractTextFormatWriter implements \
Closeable {  
     /** Write a double value formatted using the configured decimal format function.
      * @param d value to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected void write(final double d) throws IOException {
+    protected void write(final double d) {
         write(doubleFormat.apply(d));
     }
 
     /** Write an integer value.
      * @param n value to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected void write(final int n) throws IOException {
+    protected void write(final int n) {
         write(String.valueOf(n));
     }
 
     /** Write a char value.
      * @param c character to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected void write(final char c) throws IOException {
-        writer.write(c);
+    protected void write(final char c) {
+        try {
+            writer.write(c);
+        } catch (IOException exc) {
+            throw GeometryIOUtils.createUnchecked(exc);
+        }
     }
 
     /** Write a string.
      * @param str string to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected void write(final String str) throws IOException {
-        writer.write(str);
+    protected void write(final String str) {
+        try {
+            writer.write(str);
+        } catch (IOException exc) {
+            throw GeometryIOUtils.createUnchecked(exc);
+        }
     }
 
     /** Write the configured line separator to the output.
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected void writeNewLine() throws IOException {
+    protected void writeNewLine() {
         write(lineSeparator);
     }
 }
diff --git a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/BoundaryIOManagerTest.java \
b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/BoundaryIOManagerTest.java
 index 77c48ea..55912f9 100644
--- a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/BoundaryIOManagerTest.java
                
+++ b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/BoundaryIOManagerTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
@@ -484,7 +483,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testRead_formatGiven() throws IOException {
+    void testRead_formatGiven() {
         // arrange
         final StubReadHandler r1 = new StubReadHandler(FMT_A);
         manager.registerReadHandler(r1);
@@ -502,7 +501,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testRead_noFormatGiven() throws IOException {
+    void testRead_noFormatGiven() {
         // arrange
         final StubReadHandler r1 = new StubReadHandler(FMT_A);
         manager.registerReadHandler(r1);
@@ -520,7 +519,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testRead_handlerNotFound() throws IOException {
+    void testRead_handlerNotFound() {
         // arrange
         final StubReadHandler r1 = new StubReadHandler(FMT_A);
         manager.registerReadHandler(r1);
@@ -541,7 +540,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testBoundaries_formatGiven() throws IOException {
+    void testBoundaries_formatGiven() {
         // arrange
         final StubReadHandler r1 = new StubReadHandler(FMT_A);
         manager.registerReadHandler(r1);
@@ -559,7 +558,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testBoundaries_noFormatGiven() throws IOException {
+    void testBoundaries_noFormatGiven() {
         // arrange
         final StubReadHandler r1 = new StubReadHandler(FMT_A);
         manager.registerReadHandler(r1);
@@ -577,7 +576,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testBoundaries_handlerNotFound() throws IOException {
+    void testBoundaries_handlerNotFound() {
         // arrange
         final StubReadHandler r1 = new StubReadHandler(FMT_A);
         manager.registerReadHandler(r1);
@@ -598,7 +597,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testWrite_formatGiven() throws IOException {
+    void testWrite_formatGiven() {
         // arrange
         final StubWriteHandler w1 = new StubWriteHandler(FMT_A);
         manager.registerWriteHandler(w1);
@@ -615,7 +614,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testWrite_noFormatGiven() throws IOException {
+    void testWrite_noFormatGiven() {
         // arrange
         final StubWriteHandler w1 = new StubWriteHandler(FMT_A);
         manager.registerWriteHandler(w1);
@@ -632,7 +631,7 @@ class BoundaryIOManagerTest {
     }
 
     @Test
-    void testWrite_handlerNotFound() throws IOException {
+    void testWrite_handlerNotFound() {
         // arrange
         final StubWriteHandler w1 = new StubWriteHandler(FMT_A);
         manager.registerWriteHandler(w1);
@@ -685,7 +684,7 @@ class BoundaryIOManagerTest {
 
         /** {@inheritDoc} */
         @Override
-        public InputStream getInputStream() throws IOException {
+        public InputStream getInputStream() {
             throw new UnsupportedOperationException();
         }
 
@@ -713,7 +712,7 @@ class BoundaryIOManagerTest {
 
         /** {@inheritDoc} */
         @Override
-        public OutputStream getOutputStream() throws IOException {
+        public OutputStream getOutputStream() {
             throw new UnsupportedOperationException();
         }
     }
@@ -738,8 +737,7 @@ class BoundaryIOManagerTest {
 
         /** {@inheritDoc} */
         @Override
-        public TestBoundaryList read(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-                throws IOException {
+        public TestBoundaryList read(final GeometryInput in, final \
Precision.DoubleEquivalence precision) {  this.inArg = in;
             this.precisionArg = precision;
 
@@ -749,7 +747,7 @@ class BoundaryIOManagerTest {
         /** {@inheritDoc} */
         @Override
         public Stream<TestLineSegment> boundaries(final GeometryInput in,
-                final Precision.DoubleEquivalence precision) throws IOException {
+                final Precision.DoubleEquivalence precision) {
             this.inArg = in;
             this.precisionArg = precision;
 
@@ -777,7 +775,7 @@ class BoundaryIOManagerTest {
 
         /** {@inheritDoc} */
         @Override
-        public void write(final TestBoundaryList boundarySource, final \
GeometryOutput out) throws IOException { +        public void write(final \
TestBoundaryList boundarySource, final GeometryOutput out) {  this.list = \
boundarySource;  this.outArg = out;
         }
diff --git a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/input/UrlGeometryInputTest.java \
b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/input/UrlGeometryInputTest.java
 index 1d512a2..77f0aca 100644
--- a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/input/UrlGeometryInputTest.java
                
+++ b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/input/UrlGeometryInputTest.java
 @@ -50,7 +50,7 @@ class UrlGeometryInputTest {
     }
 
     @Test
-    void testCtor_fileAndCharset() throws IOException {
+    void testCtor_fileAndCharset() {
         // arrange
         final URL url = getClass().getResource("/java/lang/String.class");
         final Charset charset = StandardCharsets.UTF_8;
diff --git a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/CharReadBufferTest.java \
b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/CharReadBufferTest.java
 index 65e1168..009acda 100644
--- a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/CharReadBufferTest.java
                
+++ b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/CharReadBufferTest.java
 @@ -19,6 +19,7 @@ package org.apache.commons.geometry.io.core.internal;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
+import java.io.UncheckedIOException;
 import java.util.Random;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
@@ -44,7 +45,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testHasMoreCharacters() throws IOException {
+    void testHasMoreCharacters() {
         // act/assert
         for (int s = 1; s < 10; s += 2) {
             Assertions.assertFalse(new \
CharReadBuffer(reader("")).hasMoreCharacters()); @@ -65,7 +66,7 @@ class \
CharReadBufferTest {  }
 
     @Test
-    void testPeekRead() throws IOException {
+    void testPeekRead() {
         // arrange
         final String str = "abcdefg";
         final CharReadBuffer buf = new CharReadBuffer(reader(str), 1);
@@ -88,7 +89,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testCharAt() throws IOException {
+    void testCharAt() {
         // arrange
         final String str = "abcdefgh";
         final CharReadBuffer buf = new CharReadBuffer(reader(str), 3);
@@ -109,7 +110,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testCharAt_invalidArg() throws IOException {
+    void testCharAt_invalidArg() {
         // arrange
         final String str = "abcdefgh";
         final CharReadBuffer buf = new CharReadBuffer(reader(str), 3);
@@ -121,7 +122,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testReadPeek_string() throws IOException {
+    void testReadPeek_string() {
         // arrange
         final String str = "abcdefgh";
         final CharReadBuffer buf = new CharReadBuffer(reader(str), 50);
@@ -141,7 +142,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testReadPeek_tring_zeroLen() throws IOException {
+    void testReadPeek_tring_zeroLen() {
         // act/assert
         Assertions.assertNull(new CharReadBuffer(reader("")).peekString(0));
         Assertions.assertNull(new CharReadBuffer(reader("")).readString(0));
@@ -151,7 +152,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testReadPeek_string_invalidArg() throws IOException {
+    void testReadPeek_string_invalidArg() {
         // arrange
         final CharReadBuffer buf = new CharReadBuffer(reader("a"));
         final String msg = "Requested string length cannot be negative; was -1";
@@ -167,7 +168,23 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testSkip() throws IOException {
+    void testReadPeek_failure() {
+        // arrange
+        final CharReadBuffer buf = new CharReadBuffer(failReader());
+        final String msg = "IOException: read";
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            buf.peekString(3);
+        }, UncheckedIOException.class, msg);
+
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            buf.readString(3);
+        }, UncheckedIOException.class, msg);
+    }
+
+    @Test
+    void testSkip() {
         // arrange
         final CharReadBuffer buf = new CharReadBuffer(reader("abcdefg"), 3);
         buf.peekString(2);
@@ -193,7 +210,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testSkip_invalidArg() throws IOException {
+    void testSkip_invalidArg() {
         // arrange
         final CharReadBuffer buf = new CharReadBuffer(reader("a"));
 
@@ -204,7 +221,18 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testPushString_emptyReader() throws IOException {
+    void testSkip_failure() {
+        // arrange
+        final CharReadBuffer buf = new CharReadBuffer(failReader());
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            buf.skip(10);
+        }, UncheckedIOException.class, "IOException: skip");
+    }
+
+    @Test
+    void testPushString_emptyReader() {
         // arrange
         final String a = "abcd";
         final String b = "efgh";
@@ -220,7 +248,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testPushString_nonEmptyReader() throws IOException {
+    void testPushString_nonEmptyReader() {
         // arrange
         final String a = "abcd";
         final String b = "efgh";
@@ -236,7 +264,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testPush_emptyReader() throws IOException {
+    void testPush_emptyReader() {
         // arrange
         final CharReadBuffer buf = new CharReadBuffer(reader("ABCD"), 1);
 
@@ -252,7 +280,7 @@ class CharReadBufferTest {
     }
 
     @Test
-    void testAlternatingPushAndRead() throws IOException {
+    void testAlternatingPushAndRead() {
         // arrange
         final String str = repeat("abcdefghijlmnopqrstuvwxyz", 10);
 
@@ -281,6 +309,25 @@ class CharReadBufferTest {
         return new StringReader(content);
     }
 
+    private static Reader failReader() {
+        return new Reader() {
+
+            @Override
+            public int read(char[] cbuf, int off, int len) throws IOException {
+                throw new IOException("read");
+            }
+
+            @Override
+            public long skip(final long skip) throws IOException {
+                throw new IOException("skip");
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+    }
+
     private static String repeat(final String str, final int count) {
         final StringBuilder sb = new StringBuilder();
         for (int i = 0; i < count; ++i) {
diff --git a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtilsTest.java \
b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtilsTest.java
 index f5911dd..f9ffcca 100644
--- a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtilsTest.java
                
+++ b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/GeometryIOUtilsTest.java
 @@ -16,12 +16,16 @@
  */
 package org.apache.commons.geometry.io.core.internal;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
@@ -29,6 +33,10 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.io.core.input.GeometryInput;
+import org.apache.commons.geometry.io.core.input.StreamGeometryInput;
+import org.apache.commons.geometry.io.core.output.GeometryOutput;
+import org.apache.commons.geometry.io.core.output.StreamGeometryOutput;
 import org.apache.commons.geometry.io.core.test.CloseCountInputStream;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -83,6 +91,143 @@ class GeometryIOUtilsTest {
     }
 
     @Test
+    void testCreateBufferedWriter_givenCharset() throws IOException {
+        // arrange
+        final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        final GeometryOutput output = new StreamGeometryOutput(bytes, null, \
StandardCharsets.UTF_8); +
+        // act
+        final BufferedWriter writer = GeometryIOUtils.createBufferedWriter(output, \
StandardCharsets.ISO_8859_1); +        writer.append('\u00fc');
+        writer.flush();
+
+        // assert
+        Assertions.assertEquals("\u00fc", new String(bytes.toByteArray(), \
StandardCharsets.UTF_8)); +    }
+
+    @Test
+    void testCreateBufferedWriter_defaultCharset() throws IOException {
+        // arrange
+        final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        final GeometryOutput output = new StreamGeometryOutput(bytes);
+
+        // act
+        final BufferedWriter writer = GeometryIOUtils.createBufferedWriter(output, \
StandardCharsets.ISO_8859_1); +        writer.append('\u00fc');
+        writer.flush();
+
+        // assert
+        Assertions.assertEquals("\u00fc", new String(bytes.toByteArray(), \
StandardCharsets.ISO_8859_1)); +    }
+
+    @Test
+    void testCreateBufferedReader_givenCharset() throws IOException {
+        // arrange
+        final byte[] bytes = "\u00fc".getBytes(StandardCharsets.UTF_8);
+        final GeometryInput input = new StreamGeometryInput(
+                new ByteArrayInputStream(bytes), null, StandardCharsets.UTF_8);
+
+        // act
+        final BufferedReader reader = GeometryIOUtils.createBufferedReader(input, \
StandardCharsets.ISO_8859_1); +
+        // assert
+        Assertions.assertEquals("\u00fc", reader.readLine());
+    }
+
+    @Test
+    void testCreateBufferedReader_defaultCharset() throws IOException {
+        // arrange
+        final byte[] bytes = "\u00fc".getBytes(StandardCharsets.UTF_8);
+        final GeometryInput input = new StreamGeometryInput(new \
ByteArrayInputStream(bytes)); +
+        // act
+        final BufferedReader reader = GeometryIOUtils.createBufferedReader(input, \
StandardCharsets.UTF_8); +
+        // assert
+        Assertions.assertEquals("\u00fc", reader.readLine());
+    }
+
+    @Test
+    void testGetUnchecked() {
+        // act
+        final Object result = GeometryIOUtils.getUnchecked(() -> "abc");
+
+        // assert
+        Assertions.assertSame("abc", result);
+    }
+
+    @Test
+    void testGetUnchecked_failure() {
+        // arrange
+        final IOSupplier<String> supplier = () -> {
+            throw new IOException("test");
+        };
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(
+                () -> GeometryIOUtils.getUnchecked(supplier),
+                UncheckedIOException.class,
+                "IOException: test");
+    }
+
+    @Test
+    void testAcceptUnchecked() {
+        // arrange
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        final byte[] bytes = new byte[] {0, 1};
+
+        // act
+        GeometryIOUtils.acceptUnchecked(out::write, bytes);
+
+        // assert
+        Assertions.assertArrayEquals(bytes, out.toByteArray());
+    }
+
+    @Test
+    void testAcceptUnchecked_failure() {
+        // arrange
+        final IOConsumer<String> consumer = str -> {
+            throw new IOException(str);
+        };
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(
+                () -> GeometryIOUtils.acceptUnchecked(consumer, "arg"),
+                UncheckedIOException.class,
+                "IOException: arg");
+    }
+
+    @Test
+    void testApplyAsIntUnchecked() {
+        // arrange
+        final ByteArrayInputStream in = new ByteArrayInputStream(new byte[] {0, 1, \
2}); +        final byte[] bytes = new byte[10];
+
+        // act
+        int result = GeometryIOUtils.applyAsIntUnchecked(in::read, bytes);
+
+        // assert
+        Assertions.assertEquals(3, result);
+        Assertions.assertEquals((byte) 0, bytes[0]);
+        Assertions.assertEquals((byte) 1, bytes[1]);
+        Assertions.assertEquals((byte) 2, bytes[2]);
+    }
+
+    @Test
+    void testApplyAsIntUnchecked_failure() {
+        // arrange
+        final IOToIntFunction<String> consumer = str -> {
+            throw new IOException(str);
+        };
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(
+                () -> GeometryIOUtils.applyAsIntUnchecked(consumer, "arg"),
+                UncheckedIOException.class,
+                "IOException: arg");
+    }
+
+    @Test
     void testCreateUnchecked() {
         // arrange
         final FileNotFoundException exc = new FileNotFoundException("test");
@@ -96,7 +241,30 @@ class GeometryIOUtilsTest {
     }
 
     @Test
-    void testTryApplyCloseable() throws IOException {
+    void testParseError_noCause() {
+        // act
+        final IllegalStateException exc = GeometryIOUtils.parseError("test");
+
+        // assert
+        Assertions.assertEquals("test", exc.getMessage());
+        Assertions.assertNull(exc.getCause());
+    }
+
+    @Test
+    void testParseError_withCause() {
+        // arrange
+        final Throwable cause = new Throwable("cause");
+
+        // act
+        final IllegalStateException exc = GeometryIOUtils.parseError("test", cause);
+
+        // assert
+        Assertions.assertEquals("test", exc.getMessage());
+        Assertions.assertSame(cause, exc.getCause());
+    }
+
+    @Test
+    void testTryApplyCloseable() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(new \
ByteArrayInputStream(new byte[] {1}));  
@@ -109,7 +277,7 @@ class GeometryIOUtilsTest {
     }
 
     @Test
-    void testTryApplyCloseable_supplierThrows() throws IOException {
+    void testTryApplyCloseable_supplierThrows_ioException() {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             GeometryIOUtils.tryApplyCloseable(i -> {
@@ -117,11 +285,24 @@ class GeometryIOUtilsTest {
             }, () -> {
                 throw new IOException("supplier");
             });
-        }, IOException.class, "supplier");
+        }, UncheckedIOException.class, "IOException: supplier");
+    }
+
+    @Test
+    void testTryApplyCloseable_supplierThrows_runtimeException() {
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            GeometryIOUtils.tryApplyCloseable(i -> {
+                throw new IOException("fn");
+            }, () -> {
+                throw new RuntimeException("supplier");
+            });
+        }, RuntimeException.class, "supplier");
     }
 
+
     @Test
-    void testTryApplyCloseable_functionThrows() throws IOException {
+    void testTryApplyCloseable_functionThrows() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(new \
ByteArrayInputStream(new byte[0]));  
@@ -130,31 +311,31 @@ class GeometryIOUtilsTest {
             GeometryIOUtils.tryApplyCloseable(i -> {
                 throw new IOException("fn");
             }, () -> in);
-        }, IOException.class, "fn");
+        }, UncheckedIOException.class, "IOException: fn");
 
         Assertions.assertEquals(1, in.getCloseCount());
     }
 
     @Test
-    void testTryApplyCloseable_functionThrows_inputCloseThrows() throws IOException \
{ +    void testTryApplyCloseable_functionThrows_inputCloseThrows() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(new \
CloseFailByteArrayInputStream(new byte[0]));  
         // act/assert
-        final Throwable thr = Assertions.assertThrows(IOException.class, () -> {
+        final Throwable thr = Assertions.assertThrows(UncheckedIOException.class, () \
-> {  GeometryIOUtils.tryApplyCloseable(i -> {
                 throw new IOException("fn");
             }, () -> in);
         });
 
-        Assertions.assertEquals(IOException.class, thr.getClass());
+        Assertions.assertEquals(UncheckedIOException.class, thr.getClass());
         Assertions.assertEquals("close", thr.getSuppressed()[0].getMessage());
 
         Assertions.assertEquals(1, in.getCloseCount());
     }
 
     @Test
-    void testCreateCloseableStream() throws IOException {
+    void testCreateCloseableStream() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(new \
ByteArrayInputStream(new byte[0]));  
@@ -168,7 +349,7 @@ class GeometryIOUtilsTest {
     }
 
     @Test
-    void testCreateCloseableStream_closeThrows() throws IOException {
+    void testCreateCloseableStream_closeThrows() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(new \
CloseFailByteArrayInputStream(new byte[0]));  
diff --git a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/SimpleTextParserTest.java \
b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/SimpleTextParserTest.java
 index ac27eaf..bad8758 100644
--- a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/SimpleTextParserTest.java
                
+++ b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/internal/SimpleTextParserTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.core.internal;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.function.IntPredicate;
@@ -41,7 +40,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testMaxStringLength_illegalArg() throws IOException {
+    void testMaxStringLength_illegalArg() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -52,14 +51,14 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testCharacterSequence() throws IOException {
+    void testCharacterSequence() {
         // act/assert
         assertCharacterSequence(parser(""), "");
         assertCharacterSequence(parser("abc def"), "abc def");
     }
 
     @Test
-    void testCharacterPosition() throws IOException {
+    void testCharacterPosition() {
         // arrange
         final SimpleTextParser p = parser(
                 "a b\n" +
@@ -103,7 +102,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testCharacterPosition_givenPosition() throws IOException {
+    void testCharacterPosition_givenPosition() {
         // arrange
         final SimpleTextParser p = parser("abc\rdef");
 
@@ -125,7 +124,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testHasMoreCharacters() throws IOException {
+    void testHasMoreCharacters() {
         // arrange
         final SimpleTextParser empty = parser("");
         final SimpleTextParser nonEmpty = parser("a");
@@ -139,7 +138,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testHasMoreCharactersOnLine() throws IOException {
+    void testHasMoreCharactersOnLine() {
         // arrange
         final SimpleTextParser empty = parser("");
         final SimpleTextParser singleLine = parser("a");
@@ -181,7 +180,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testBasicTokenMethods() throws IOException {
+    void testBasicTokenMethods() {
         // arrange
         final SimpleTextParser p = parser("abcdef\r\n\r ghi");
 
@@ -212,7 +211,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testGetCurrentTokenAsDouble() throws IOException {
+    void testGetCurrentTokenAsDouble() {
         // arrange
         final SimpleTextParser p = parser("1e-4\n+5\n-4.001");
 
@@ -228,7 +227,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testGetCurrentTokenAsDouble_failures() throws IOException {
+    void testGetCurrentTokenAsDouble_failures() {
         // arrange
         final SimpleTextParser p = parser("abc\n1.1.1a");
 
@@ -240,54 +239,54 @@ class SimpleTextParserTest {
         p.next(SimpleTextParser::isNotNewLinePart);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsDouble();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 1: expected double but found \
[abc]");  
         p.nextAlphanumeric();
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsDouble();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 4: expected double but found end \
of line");  
         p.discardLine()
             .next(c -> c != 'a');
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsDouble();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 1: expected double but found \
[1.1.1]");  
         p.next(Character::isDigit);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsDouble();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 6: expected double but found empty \
token followed by [a]");  
         p.nextLine();
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsDouble();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 6: expected double but found \
[a]");  
         p.nextLine();
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsDouble();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 7: expected double but found end \
of content");  }
 
     @Test
-    void testGetCurrentTokenAsDouble_includedNumberFormatExceptionOnFailure() throws \
IOException { +    void \
testGetCurrentTokenAsDouble_includedNumberFormatExceptionOnFailure() {  // arrange
         final SimpleTextParser p = parser("abc");
         p.nextLine();
 
         // act/assert
-        final Throwable exc = Assertions.assertThrows(IOException.class, () -> \
p.getCurrentTokenAsDouble()); +        final Throwable exc = \
Assertions.assertThrows(IllegalStateException.class, () -> \
                p.getCurrentTokenAsDouble());
         Assertions.assertEquals(NumberFormatException.class, \
exc.getCause().getClass());  }
 
     @Test
-    void testGetCurrentTokenAsInt() throws IOException {
+    void testGetCurrentTokenAsInt() {
         // arrange
         final SimpleTextParser p = parser("0\n+5\n-401");
 
@@ -303,7 +302,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testGetCurrentTokenAsInt_failures() throws IOException {
+    void testGetCurrentTokenAsInt_failures() {
         // arrange
         final SimpleTextParser p = parser("abc\n1.1.1a");
 
@@ -315,54 +314,54 @@ class SimpleTextParserTest {
         p.next(SimpleTextParser::isNotNewLinePart);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsInt();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 1: expected integer but found \
[abc]");  
         p.nextAlphanumeric();
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsInt();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 4: expected integer but found end \
of line");  
         p.discardLine()
             .next(c -> c != 'a');
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsInt();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 1: expected integer but found \
[1.1.1]");  
         p.next(Character::isDigit);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsInt();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 6: expected integer but found \
empty token followed by [a]");  
         p.nextLine();
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsInt();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 6: expected integer but found \
[a]");  
         p.nextLine();
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.getCurrentTokenAsInt();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 7: expected integer but found end \
of content");  }
 
     @Test
-    void testGetCurrentTokenAsInt_includedNumberFormatExceptionOnFailure() throws \
IOException { +    void \
testGetCurrentTokenAsInt_includedNumberFormatExceptionOnFailure() {  // arrange
         final SimpleTextParser p = parser("abc");
         p.nextLine();
 
         // act/assert
-        final Throwable exc = Assertions.assertThrows(IOException.class, () -> \
p.getCurrentTokenAsInt()); +        final Throwable exc = \
                Assertions.assertThrows(IllegalStateException.class, () -> \
                p.getCurrentTokenAsInt());
         Assertions.assertEquals(NumberFormatException.class, \
exc.getCause().getClass());  }
 
     @Test
-    void testNext_lenArg() throws IOException {
+    void testNext_lenArg() {
         // arrange
         final SimpleTextParser p = parser("abcdef\r\n\r ghi");
 
@@ -377,7 +376,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testNextWithLineContinuation_lenArg() throws IOException {
+    void testNextWithLineContinuation_lenArg() {
         // arrange
         final char cont = '\\';
         final SimpleTextParser p = parser("a\\bcdef\\\r\n\r ghi\\\n\\\n\\\rj");
@@ -393,7 +392,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testNext_lenArg_invalidArg() throws IOException {
+    void testNext_lenArg_invalidArg() {
         // arrange
         final SimpleTextParser p = parser("abc");
         p.setMaxStringLength(2);
@@ -409,7 +408,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testNext_predicateArg() throws IOException {
+    void testNext_predicateArg() {
         // arrange
         final SimpleTextParser p = parser("a\n 012\r\ndef");
 
@@ -433,7 +432,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testNext_predicateArg_exceedsMaxStringLength() throws IOException {
+    void testNext_predicateArg_exceedsMaxStringLength() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
         p.setMaxStringLength(4);
@@ -441,11 +440,11 @@ class SimpleTextParserTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.next(c -> !Character.isWhitespace(c));
-        }, IOException.class, "Parsing failed at line 1, column 1: string length \
exceeds maximum value of 4"); +        }, IllegalStateException.class, "Parsing \
failed at line 1, column 1: string length exceeds maximum value of 4");  }
 
     @Test
-    void testNextWithLineContinuation_predicateArg() throws IOException {
+    void testNextWithLineContinuation_predicateArg() {
         // arrange
         final char cont = '|';
         final SimpleTextParser p = parser("|\na\n 0|\r\n|\r12\r\nd|ef");
@@ -470,7 +469,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testNextLine() throws IOException {
+    void testNextLine() {
         // arrange
         final SimpleTextParser p = parser("a\n 012\r\ndef\n\nx");
 
@@ -489,7 +488,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testNextAlphanumeric() throws IOException {
+    void testNextAlphanumeric() {
         // arrange
         final SimpleTextParser p = parser("a10Fd;X23456789-0\ny");
 
@@ -511,7 +510,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscard_lenArg() throws IOException {
+    void testDiscard_lenArg() {
         // arrange
         final SimpleTextParser p = parser("\na,b c\r\n12.3\rdef\n");
 
@@ -542,7 +541,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscardWithLineContinuation_lenArg() throws IOException {
+    void testDiscardWithLineContinuation_lenArg() {
         // arrange
         final char cont = '|';
         final SimpleTextParser p = parser("\n|a|\r\n,b|\n|\r c\r\n12.3\rdef\n");
@@ -574,7 +573,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscard_predicateArg() throws IOException {
+    void testDiscard_predicateArg() {
         // arrange
         final SimpleTextParser p = parser("\na,b c\r\n12.3\rdef\n");
 
@@ -609,7 +608,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscardWithLineContinuation_predicateArg() throws IOException {
+    void testDiscardWithLineContinuation_predicateArg() {
         // arrange
         final char cont = '|';
         final SimpleTextParser p = parser("\na,|\r\nb |c\r\n1|\r|\n2.3\rdef\n");
@@ -645,7 +644,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscardWhitespace() throws IOException {
+    void testDiscardWhitespace() {
         // arrange
         final SimpleTextParser p = parser("a\t\n\r\n   b c");
 
@@ -668,7 +667,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscardLineWhitespace() throws IOException {
+    void testDiscardLineWhitespace() {
         // arrange
         final SimpleTextParser p = parser("a\t\n\r\n   b c");
 
@@ -707,7 +706,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscardNewLineSequence() throws IOException {
+    void testDiscardNewLineSequence() {
         // arrange
         final SimpleTextParser p = parser("a\t\n\r\n   b\rc");
 
@@ -742,7 +741,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testDiscardLine() throws IOException {
+    void testDiscardLine() {
         // arrange
         final SimpleTextParser p = parser("a\t\n\r\n   b c");
 
@@ -765,7 +764,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testPeek_lenArg() throws IOException {
+    void testPeek_lenArg() {
         // arrange
         final SimpleTextParser p = parser("abcdef\r\n\r ghi");
 
@@ -796,7 +795,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testPeek_lenArg_invalidArg() throws IOException {
+    void testPeek_lenArg_invalidArg() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
         p.setMaxStringLength(4);
@@ -812,7 +811,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testPeek_predicateArg() throws IOException {
+    void testPeek_predicateArg() {
         // arrange
         final SimpleTextParser p = parser("abcdef\r\n\r ghi");
 
@@ -837,7 +836,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testPeek_predicateArg_exceedsMaxStringLength() throws IOException {
+    void testPeek_predicateArg_exceedsMaxStringLength() {
         // arrange
         final SimpleTextParser p = parser("\n  abcdefg");
         p.setMaxStringLength(4);
@@ -847,11 +846,11 @@ class SimpleTextParserTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.peek(SimpleTextParser::isNotWhitespace);
-        }, IOException.class, "Parsing failed at line 2, column 3: string length \
exceeds maximum value of 4"); +        }, IllegalStateException.class, "Parsing \
failed at line 2, column 3: string length exceeds maximum value of 4");  }
 
     @Test
-    void testMatch() throws IOException {
+    void testMatch() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -865,7 +864,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testMatch_failure() throws IOException {
+    void testMatch_failure() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -877,19 +876,19 @@ class SimpleTextParserTest {
         p.next(1);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.match("b");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected [b] but \
found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 1: expected [b] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.match("A");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected [A] but \
found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 1: expected [A] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.match(null);
-        }, IOException.class, "Parsing failed at line 1, column 1: expected [null] \
but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 1: expected [null] but found [a]");  }
 
     @Test
-    void testMatch_ignoreCase() throws IOException {
+    void testMatch_ignoreCase() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -903,7 +902,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testMatchIgnoreCase_failure() throws IOException {
+    void testMatchIgnoreCase_failure() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -915,15 +914,15 @@ class SimpleTextParserTest {
         p.next(1);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.matchIgnoreCase("b");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected [b] but \
found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 1: expected [b] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.match(null);
-        }, IOException.class, "Parsing failed at line 1, column 1: expected [null] \
but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 1: expected [null] but found [a]");  }
 
     @Test
-    void testTryMatch() throws IOException {
+    void testTryMatch() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -944,7 +943,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTryMatch_noToken() throws IOException {
+    void testTryMatch_noToken() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -955,7 +954,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTryMatchIgnoreCase() throws IOException {
+    void testTryMatchIgnoreCase() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -975,7 +974,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTryMatchIgnoreCase_noToken() throws IOException {
+    void testTryMatchIgnoreCase_noToken() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -986,7 +985,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testChoose() throws IOException {
+    void testChoose() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -1007,7 +1006,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testChoose_failure() throws IOException {
+    void testChoose_failure() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -1019,23 +1018,23 @@ class SimpleTextParserTest {
         p.next(1);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.choose("X");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[X] but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line \
1, column 1: expected one of [X] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.choose("X", "Y", "Z");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[X, Y, Z] but found [a]"); +        }, IllegalStateException.class, "Parsing failed \
at line 1, column 1: expected one of [X, Y, Z] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.choose("A");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[A] but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line \
1, column 1: expected one of [A] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.choose();
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[] but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line \
1, column 1: expected one of [] but found [a]");  }
 
     @Test
-    void testChooseIgnoreCase() throws IOException {
+    void testChooseIgnoreCase() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -1056,7 +1055,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testChooseIgnoreCase_failure() throws IOException {
+    void testChooseIgnoreCase_failure() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -1068,19 +1067,19 @@ class SimpleTextParserTest {
         p.next(1);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.chooseIgnoreCase("X");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[X] but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line \
1, column 1: expected one of [X] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.chooseIgnoreCase("X", "Y", "Z");
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[X, Y, Z] but found [a]"); +        }, IllegalStateException.class, "Parsing failed \
at line 1, column 1: expected one of [X, Y, Z] but found [a]");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.chooseIgnoreCase();
-        }, IOException.class, "Parsing failed at line 1, column 1: expected one of \
[] but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line \
1, column 1: expected one of [] but found [a]");  }
 
     @Test
-    void testTryChoose() throws IOException {
+    void testTryChoose() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -1105,7 +1104,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTryChoose_noToken() throws IOException {
+    void testTryChoose_noToken() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -1116,7 +1115,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTryChooseIgnoreCase() throws IOException {
+    void testTryChooseIgnoreCase() {
         // arrange
         final SimpleTextParser p = parser("abc");
 
@@ -1141,7 +1140,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTryChooseIgnoreCase_noToken() throws IOException {
+    void testTryChooseIgnoreCase_noToken() {
         // arrange
         final SimpleTextParser p = parser("abcdef");
 
@@ -1152,7 +1151,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testUnexpectedToken() throws IOException {
+    void testUnexpectedToken() {
         // arrange
         final SimpleTextParser p = parser("abc\ndef");
 
@@ -1180,7 +1179,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testUnexpectedToken_causeArg() throws IOException {
+    void testUnexpectedToken_causeArg() {
         // arrange
         final SimpleTextParser p = parser("abc");
         final Exception cause = new Exception("test");
@@ -1188,14 +1187,14 @@ class SimpleTextParserTest {
         // act/assert
         p.nextLine();
 
-        IOException exc = p.unexpectedToken("test", cause);
+        final IllegalStateException exc = p.unexpectedToken("test", cause);
         Assertions.assertEquals("Parsing failed at line 1, column 1: expected test \
but found [abc]",  exc.getMessage());
         Assertions.assertSame(cause, exc.getCause());
     }
 
     @Test
-    void testUnexpectedToken_ioError() throws IOException {
+    void testUnexpectedToken_ioError() {
         // arrange
         final FailBuffer b = new FailBuffer(new StringReader("abc"));
         final SimpleTextParser p = new SimpleTextParser(b);
@@ -1221,7 +1220,7 @@ class SimpleTextParserTest {
     }
 
     @Test
-    void testTokenError() throws IOException {
+    void testTokenError() {
         // arrange
         final SimpleTextParser p = parser("a\nbc");
         p.nextLine();
@@ -1229,27 +1228,27 @@ class SimpleTextParserTest {
         p.readChar();
 
         // act/assert
-        final IOException exc = p.tokenError("test message");
+        final IllegalStateException exc = p.tokenError("test message");
 
         Assertions.assertEquals("Parsing failed at line 2, column 1: test message", \
exc.getMessage());  Assertions.assertNull(exc.getCause());
     }
 
     @Test
-    void testTokenError_noTokenSet() throws IOException {
+    void testTokenError_noTokenSet() {
         // arrange
         final SimpleTextParser p = parser("ab\nc");
         p.readChar();
 
         // act/assert
-        final IOException exc = p.tokenError("test message");
+        final IllegalStateException exc = p.tokenError("test message");
 
         Assertions.assertEquals("Parsing failed at line 1, column 2: test message", \
exc.getMessage());  Assertions.assertNull(exc.getCause());
     }
 
     @Test
-    void testTokenError_withCause() throws IOException {
+    void testTokenError_withCause() {
         // arrange
         SimpleTextParser p = parser("a\nbc");
         p.nextLine();
@@ -1259,34 +1258,34 @@ class SimpleTextParserTest {
         final Exception cause = new Exception("test");
 
         // act/assert
-        final IOException exc = p.tokenError("test message", cause);
+        final IllegalStateException exc = p.tokenError("test message", cause);
 
         Assertions.assertEquals("Parsing failed at line 2, column 1: test message", \
exc.getMessage());  Assertions.assertSame(cause, exc.getCause());
     }
 
     @Test
-    void testParseError_currentLineCol() throws IOException {
+    void testParseError_currentLineCol() {
         // arrange
         final SimpleTextParser p = parser("a\nbc");
         p.discard(ch -> ch != 'b');
 
         // act
-        final IOException exc = p.parseError("test message");
+        final IllegalStateException exc = p.parseError("test message");
 
         Assertions.assertEquals("Parsing failed at line 2, column 1: test message", \
exc.getMessage());  Assertions.assertNull(exc.getCause());
     }
 
     @Test
-    void testParseError_currentLineCol_withCause() throws IOException {
+    void testParseError_currentLineCol_withCause() {
         // arrange
         final SimpleTextParser p = parser("abc");
         p.readChar();
         final Exception cause = new Exception("test");
 
         // act
-        final IOException exc = p.parseError("test message", cause);
+        final IllegalStateException exc = p.parseError("test message", cause);
 
         Assertions.assertEquals("Parsing failed at line 1, column 2: test message", \
exc.getMessage());  Assertions.assertSame(cause, exc.getCause());
@@ -1298,7 +1297,7 @@ class SimpleTextParserTest {
         final SimpleTextParser p = parser("abc");
 
         // act
-        final IOException exc = p.parseError(5, 6, "test message");
+        final IllegalStateException exc = p.parseError(5, 6, "test message");
 
         Assertions.assertEquals("Parsing failed at line 5, column 6: test message", \
exc.getMessage());  Assertions.assertNull(exc.getCause());
@@ -1311,7 +1310,7 @@ class SimpleTextParserTest {
         final Exception cause = new Exception("test");
 
         // act
-        final IOException exc = p.parseError(5, 6, "test message", cause);
+        final IllegalStateException exc = p.parseError(5, 6, "test message", cause);
 
         Assertions.assertEquals("Parsing failed at line 5, column 6: test message", \
exc.getMessage());  Assertions.assertSame(cause, exc.getCause());
@@ -1354,8 +1353,7 @@ class SimpleTextParserTest {
         return new SimpleTextParser(reader);
     }
 
-    private static void assertCharacterSequence(final SimpleTextParser parser, final \
                String expected)
-            throws IOException {
+    private static void assertCharacterSequence(final SimpleTextParser parser, final \
String expected) {  char expectedChar;
         String msg;
         for (int i = 0; i < expected.length(); ++i) {
@@ -1436,14 +1434,14 @@ class SimpleTextParserTest {
         }
 
         @Override
-        public boolean hasMoreCharacters() throws IOException {
+        public boolean hasMoreCharacters() {
             checkFail();
             return super.hasMoreCharacters();
         }
 
-        private void checkFail() throws IOException {
+        private void checkFail() {
             if (fail) {
-                throw new IOException("test failure");
+                throw new IllegalStateException("test failure");
             }
         }
     }
diff --git a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriterTest.java \
b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriterTest.java
 index 5f9516f..5b916e9 100644
--- a/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriterTest.java
                
+++ b/commons-geometry-io-core/src/test/java/org/apache/commons/geometry/io/core/utils/AbstractTextFormatWriterTest.java
 @@ -18,12 +18,14 @@ package org.apache.commons.geometry.io.core.utils;
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.io.UncheckedIOException;
 import java.io.Writer;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.util.Locale;
 import java.util.function.DoubleFunction;
 
+import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.io.core.test.CloseCountWriter;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -33,7 +35,7 @@ class AbstractTextFormatWriterTest {
     private StringWriter out = new StringWriter();
 
     @Test
-    void testDefaults() throws IOException {
+    void testDefaults() {
         // act
         try (TestWriter writer = new TestWriter(out)) {
             // assert
@@ -44,7 +46,7 @@ class AbstractTextFormatWriterTest {
     }
 
     @Test
-    void testWrite_defaultConfig() throws IOException {
+    void testWrite_defaultConfig() {
         // arrange
         final double n = 20000.0 / 3.0;
         final CloseCountWriter closeCountWriter = new CloseCountWriter(out);
@@ -67,7 +69,7 @@ class AbstractTextFormatWriterTest {
     }
 
     @Test
-    void testWrite_customConfig() throws IOException {
+    void testWrite_customConfig() {
         // arrange
         final CloseCountWriter closeCountWriter = new CloseCountWriter(out);
         try (TestWriter writer = new TestWriter(closeCountWriter)) {
@@ -94,6 +96,38 @@ class AbstractTextFormatWriterTest {
         Assertions.assertEquals(1, closeCountWriter.getCloseCount());
     }
 
+    @Test
+    void testWrite_failure() {
+        // arrange
+        final Writer failWriter = new Writer() {
+            @Override
+            public void write(char[] cbuf, int off, int len) throws IOException {
+                throw new IOException("test");
+            }
+
+            @Override
+            public void flush() {
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+
+        // act/assert
+        try (TestWriter writer = new TestWriter(failWriter)) {
+            GeometryTestUtils.assertThrowsWithMessage(
+                    () -> writer.write('a'),
+                    UncheckedIOException.class,
+                    "IOException: test");
+
+            GeometryTestUtils.assertThrowsWithMessage(
+                    () -> writer.write("abc"),
+                    UncheckedIOException.class,
+                    "IOException: test");
+        }
+    }
+
     private static final class TestWriter extends AbstractTextFormatWriter {
 
         protected TestWriter(final Writer writer) {
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3D.java
 index 94c736b..645b8e2 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -42,8 +41,7 @@ public abstract class AbstractBoundaryReadHandler3D implements \
BoundaryReadHandl  
     /** {@inheritDoc} */
     @Override
-    public BoundarySource3D read(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public BoundarySource3D read(final GeometryInput in, final \
Precision.DoubleEquivalence precision) {  // read the input as a simple list of \
boundaries  final List<PlaneConvexSubset> list = new ArrayList<>();
 
@@ -59,8 +57,7 @@ public abstract class AbstractBoundaryReadHandler3D implements \
BoundaryReadHandl  
     /** {@inheritDoc} */
     @Override
-    public TriangleMesh readTriangleMesh(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public TriangleMesh readTriangleMesh(final GeometryInput in, final \
                Precision.DoubleEquivalence precision) {
         final SimpleTriangleMesh.Builder meshBuilder = \
SimpleTriangleMesh.builder(precision);  
         try (FacetDefinitionReader reader = facetDefinitionReader(in)) {
@@ -81,15 +78,14 @@ public abstract class AbstractBoundaryReadHandler3D implements \
BoundaryReadHandl  
     /** {@inheritDoc} */
     @Override
-    public Stream<PlaneConvexSubset> boundaries(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public Stream<PlaneConvexSubset> boundaries(final GeometryInput in, final \
Precision.DoubleEquivalence precision) {  return facets(in)
                 .map(f -> FacetDefinitions.toPolygon(f, precision));
     }
 
     /** {@inheritDoc} */
     @Override
-    public Stream<FacetDefinition> facets(final GeometryInput in) throws IOException \
{ +    public Stream<FacetDefinition> facets(final GeometryInput in) {
         return GeometryIOUtils.createCloseableStream(inputStream -> {
             final FacetDefinitionReader fdReader = facetDefinitionReader(in);
             final FacetDefinitionReaderIterator it = new \
FacetDefinitionReaderIterator(fdReader); @@ -98,8 +94,7 @@ public abstract class \
AbstractBoundaryReadHandler3D implements BoundaryReadHandl  }, in::getInputStream);
     }
 
-    /** Class exposing a {@link FacetDefinitionReader} as an iterator. {@link \
                IOException}s are wrapped
-     * with {@link java.io.UncheckedIOException}.
+    /** Class exposing a {@link FacetDefinitionReader} as an iterator.
      */
     static final class FacetDefinitionReaderIterator implements \
Iterator<FacetDefinition> {  
@@ -149,16 +144,11 @@ public abstract class AbstractBoundaryReadHandler3D implements \
BoundaryReadHandl  }
         }
 
-        /** Load the next facet from the underlying reader. Any {@link IOException}s
-         * are wrapped with {@link java.io.UncheckedIOException}.
+        /** Load the next facet from the underlying reader.
          */
         private void loadNext() {
             ++loadCount;
-            try {
-                next = reader.readFacet();
-            } catch (final IOException exc) {
-                throw GeometryIOUtils.createUnchecked(exc);
-            }
+            next = reader.readFacet();
         }
     }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryWriteHandler3D.java
 index 56c6ff2..249caa4 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryWriteHandler3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.stream.Stream;
 
@@ -30,8 +29,7 @@ public abstract class AbstractBoundaryWriteHandler3D implements \
BoundaryWriteHan  
     /** {@inheritDoc} */
     @Override
-    public void write(final BoundarySource3D src, final GeometryOutput out)
-            throws IOException {
+    public void write(final BoundarySource3D src, final GeometryOutput out) {
         try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
             write(stream, out);
         }
@@ -39,8 +37,7 @@ public abstract class AbstractBoundaryWriteHandler3D implements \
BoundaryWriteHan  
     /** {@inheritDoc} */
     @Override
-    public void writeFacets(final Collection<? extends FacetDefinition> facets, \
                final GeometryOutput out)
-            throws IOException {
+    public void writeFacets(final Collection<? extends FacetDefinition> facets, \
final GeometryOutput out) {  writeFacets(facets.stream(), out);
     }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3D.java
 index 88de395..f444ac3 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.stream.Stream;
 
@@ -41,8 +40,8 @@ import org.apache.commons.numbers.core.Precision;
 /** Class managing IO operations for geometric data formats containing 3D region \
                boundaries.
  * IO operation are performed by read and write handlers registered for specific \
                data formats.
  *
- * <p>Instances of this class are thread-safe as long as the registered handler \
                instances are
- * thread-safe.</p>
+ * <p><strong>Implementation note:</strong>Instances of this class are thread-safe \
as long as the + * registered handler instances are thread-safe.</p>
  * @see BoundaryReadHandler3D
  * @see BoundaryWriteHandler3D
  * @see <a href="https://en.wikipedia.org/wiki/Boundary_representations">Boundary \
representations</a> @@ -59,10 +58,10 @@ public class BoundaryIOManager3D extends \
                BoundaryIOManager<
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @return facet definition reader
      * @throws IllegalArgumentException if no read handler can be found for the \
                input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public FacetDefinitionReader facetDefinitionReader(final GeometryInput in, final \
                GeometryFormat fmt)
-            throws IOException {
+    public FacetDefinitionReader facetDefinitionReader(final GeometryInput in, final \
GeometryFormat fmt) {  return requireReadHandler(in, fmt).facetDefinitionReader(in);
     }
 
@@ -74,16 +73,20 @@ public class BoundaryIOManager3D extends BoundaryIOManager<
      *      // access stream content
      *  }
      * </pre>
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
java.io.UncheckedIOException}.</p> +     * <p>The following exceptions may be thrown \
during stream iteration:</p> +     * <ul>
+     *  <li>{@link IllegalStateException} if a data format error occurs</li>
+     *  <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O \
error occurs</li> +     * </ul>
      * @param in input to read from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @return stream providing access to the facets in the input
      * @throws IllegalArgumentException if no read handler can be found for the \
                input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      */
-    public Stream<FacetDefinition> facets(final GeometryInput in, final \
GeometryFormat fmt) throws IOException { +    public Stream<FacetDefinition> \
facets(final GeometryInput in, final GeometryFormat fmt) {  return \
requireReadHandler(in, fmt).facets(in);  }
 
@@ -95,35 +98,40 @@ public class BoundaryIOManager3D extends BoundaryIOManager<
      *      // access stream content
      *  }
      * </pre>
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration:</p> +     * <ul>
+     *  <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *  <li>{@link IllegalStateException} if a data format error \
occurs</li> +     *  <li>{@link java.io.UncheckedIOException UncheckedIOException} if \
an I/O error occurs</li> +     * </ul>
      * @param in input to read from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to the triangles in the input
      * @throws IllegalArgumentException if no read handler can be found for the \
                input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      */
     public Stream<Triangle3D> triangles(final GeometryInput in, final GeometryFormat \
                fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return boundaries(in, fmt, precision)
                 .flatMap(p -> p.toTriangles().stream());
     }
 
     /** Return a {@link TriangleMesh} containing all triangles from the given input.
-     * A runtime exception may be thrown if mathematically invalid boundaries are \
                encountered.
      * @param in input to read from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @param precision precision context used for floating point comparisons
      * @return mesh containing all triangles from the input
-     * @throws IllegalArgumentException if no read handler can be found for the \
                input format
-     * @throws IOException if an I/O error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read +     *      handler can be found for the input format
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public TriangleMesh readTriangleMesh(final GeometryInput in, final \
                GeometryFormat fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return requireReadHandler(in, fmt).readTriangleMesh(in, precision);
     }
 
@@ -137,10 +145,10 @@ public class BoundaryIOManager3D extends BoundaryIOManager<
      * @param fmt format of the output; if null, the format is determined implicitly \
                from the
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler can be found for the \
                output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public void write(final Stream<? extends PlaneConvexSubset> boundaries, final \
                GeometryOutput out,
-            final GeometryFormat fmt) throws IOException {
+            final GeometryFormat fmt) {
         requireWriteHandler(out, fmt).write(boundaries, out);
     }
 
@@ -154,10 +162,10 @@ public class BoundaryIOManager3D extends BoundaryIOManager<
      * @param fmt format of the output; if null, the format is determined implicitly \
                from the
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler can be found for the \
                output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
                GeometryOutput out,
-            final GeometryFormat fmt) throws IOException {
+            final GeometryFormat fmt) {
         requireWriteHandler(out, fmt).writeFacets(facets, out);
     }
 
@@ -167,10 +175,10 @@ public class BoundaryIOManager3D extends BoundaryIOManager<
      * @param fmt format of the output; if null, the format is determined implicitly \
                from the
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler can be found for the \
                output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public void writeFacets(final Collection<? extends FacetDefinition> facets, \
                final GeometryOutput out,
-            final GeometryFormat fmt) throws IOException {
+            final GeometryFormat fmt) {
         requireWriteHandler(out, fmt).writeFacets(facets, out);
     }
 
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryReadHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryReadHandler3D.java
 index 8a0138f..0c8bb50 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryReadHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryReadHandler3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.util.stream.Stream;
 
 import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
@@ -44,9 +43,10 @@ public interface BoundaryReadHandler3D extends \
                BoundaryReadHandler<PlaneConvexSu
      * input stream.
      * @param in input stream to read from
      * @return facet definition reader instance
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    FacetDefinitionReader facetDefinitionReader(GeometryInput in) throws \
IOException; +    FacetDefinitionReader facetDefinitionReader(GeometryInput in);
 
     /** Return a {@link Stream} that can be used to access all facet information \
                from the given input stream.
      * The input stream is expected to contain data in the format supported by this \
handler. @@ -60,20 +60,25 @@ public interface BoundaryReadHandler3D extends \
                BoundaryReadHandler<PlaneConvexSu
      *  }
      * </pre>
      *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
java.io.UncheckedIOException}.</p> +     * <p>The following exceptions may be thrown \
during stream iteration:</p> +     * <ul>
+     *  <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *  <li>{@link IllegalStateException} if a parsing or syntax \
error occurs</li> +     *  <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     * </ul>
      * @param in input stream to read from; this is <em>not</em> closed when the \
                returned stream is closed
      * @return stream providing access to the facet information from the given input \
                stream
-     * @throws IOException if an I/O or data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      */
-    Stream<FacetDefinition> facets(GeometryInput in) throws IOException;
+    Stream<FacetDefinition> facets(GeometryInput in);
 
     /** Read a triangle mesh from the given input. Implementations may throw runtime
      * exceptions if mathematically invalid boundaries are encountered.
      * @param in input stream to read from
      * @param precision precision context used for floating point comparisons
      * @return triangle mesh containing the data from the given input stream
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a parsing or syntax error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    TriangleMesh readTriangleMesh(GeometryInput in, Precision.DoubleEquivalence \
precision) throws IOException; +    TriangleMesh readTriangleMesh(GeometryInput in, \
Precision.DoubleEquivalence precision);  }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryWriteHandler3D.java
 index dba8c20..1d272a6 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryWriteHandler3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.stream.Stream;
 
@@ -43,17 +42,17 @@ public interface BoundaryWriteHandler3D extends \
                BoundaryWriteHandler<PlaneConvex
      * for closing the stream if necessary (for example, if the stream fetches data \
                from the file system).
      * @param boundaries stream containing boundaries to write
      * @param out output to write to
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    void write(Stream<? extends PlaneConvexSubset> boundaries, GeometryOutput out) \
throws IOException; +    void write(Stream<? extends PlaneConvexSubset> boundaries, \
GeometryOutput out);  
     /** Write all {@link FacetDefinition facets} in the collection to the output \
                using the data format
      * supported by this instance.
      * @param facets facets to write
      * @param out output to write to
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    void writeFacets(Collection<? extends FacetDefinition> facets, GeometryOutput \
out) throws IOException; +    void writeFacets(Collection<? extends FacetDefinition> \
facets, GeometryOutput out);  
     /** Write all {@link FacetDefinition facets} in the stream to the output using \
                the data format
      * supported by this instance. The stream passed as an argument is <em>not</em> \
closed, meaning @@ -61,7 +60,7 @@ public interface BoundaryWriteHandler3D extends \
                BoundaryWriteHandler<PlaneConvex
      * fetches data from the file system).
      * @param facets stream containing facets to write
      * @param out output to write to
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    void writeFacets(Stream<? extends FacetDefinition> facets, GeometryOutput out) \
throws IOException; +    void writeFacets(Stream<? extends FacetDefinition> facets, \
GeometryOutput out);  }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
 index be338b6..185243a 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/FacetDefinitionReader.java
 @@ -16,19 +16,23 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.Closeable;
-import java.io.IOException;
-
 /** Interface for reading {@link FacetDefinition facet definitions} from an input \
                source.
  * @see FacetDefinition
  */
-public interface FacetDefinitionReader extends Closeable {
+public interface FacetDefinitionReader extends AutoCloseable {
 
     /** Return the next facet definition from the input source or null if no more
      * facets are available.
      * @return the next facet definition or null if no more facets
      *      are available
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    FacetDefinition readFacet();
+
+    /** Close this instance and release all associated resources.
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    FacetDefinition readFacet() throws IOException;
+    @Override
+    void close();
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/IO3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/IO3D.java
 index 3651195..3c89c33 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/IO3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/IO3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.net.URL;
 import java.nio.file.Path;
 import java.util.Collection;
@@ -66,10 +65,11 @@ public final class IO3D {
      * @return facet definition reader
      * @throws IllegalArgumentException if no handler has been registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#facetDefinitionReader(GeometryInput, GeometryFormat)
      */
-    public static FacetDefinitionReader facetDefinitionReader(final Path path) \
throws IOException { +    public static FacetDefinitionReader \
facetDefinitionReader(final Path path) {  return facetDefinitionReader(new \
FileGeometryInput(path), null);  }
 
@@ -79,10 +79,11 @@ public final class IO3D {
      * @return facet definition reader
      * @throws IllegalArgumentException if no handler has been registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#facetDefinitionReader(GeometryInput, GeometryFormat)
      */
-    public static FacetDefinitionReader facetDefinitionReader(final URL url) throws \
IOException { +    public static FacetDefinitionReader facetDefinitionReader(final \
URL url) {  return facetDefinitionReader(new UrlGeometryInput(url), null);
     }
 
@@ -93,11 +94,11 @@ public final class IO3D {
      * @return facet definition reader
      * @throws IllegalArgumentException if no handler has been registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#facetDefinitionReader(GeometryInput, GeometryFormat)
      */
-    public static FacetDefinitionReader facetDefinitionReader(final GeometryInput \
                in, final GeometryFormat fmt)
-            throws IOException {
+    public static FacetDefinitionReader facetDefinitionReader(final GeometryInput \
in, final GeometryFormat fmt) {  return getDefaultManager().facetDefinitionReader(in, \
fmt);  }
 
@@ -113,17 +114,20 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
java.io.UncheckedIOException}.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalStateException} if a data format error occurs</li>
+     *      <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O \
error occurs</li> +     *  </ul>
      * @param path file path to read from
      * @return stream providing access to the facets in the specified file
      * @throws IllegalArgumentException if no handler has been registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#facets(GeometryInput, GeometryFormat)
      */
-    public static Stream<FacetDefinition> facets(final Path path) throws IOException \
{ +    public static Stream<FacetDefinition> facets(final Path path) {
         return facets(new FileGeometryInput(path), null);
     }
 
@@ -139,17 +143,20 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
java.io.UncheckedIOException}.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalStateException} if a data format error occurs</li>
+     *      <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O \
error occurs</li> +     *  </ul>
      * @param url URL to read from
      * @return stream providing access to the facets from the specified URL
      * @throws IllegalArgumentException if no handler has been registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#facets(GeometryInput, GeometryFormat)
      */
-    public static Stream<FacetDefinition> facets(final URL url) throws IOException {
+    public static Stream<FacetDefinition> facets(final URL url) {
         return facets(new UrlGeometryInput(url), null);
     }
 
@@ -161,19 +168,22 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
java.io.UncheckedIOException}.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalStateException} if a data format error occurs</li>
+     *      <li>{@link java.io.UncheckedIOException UncheckedIOException} if an I/O \
error occurs</li> +     *  </ul>
      * @param in input to read from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @return stream providing access to the facets in the input
      * @throws IllegalArgumentException if no read handler has been registered with \
                the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#facets(GeometryInput, GeometryFormat)
      */
-    public static Stream<FacetDefinition> facets(final GeometryInput in, final \
                GeometryFormat fmt)
-            throws IOException {
+    public static Stream<FacetDefinition> facets(final GeometryInput in, final \
GeometryFormat fmt) {  return getDefaultManager().facets(in, fmt);
     }
 
@@ -189,20 +199,22 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param path file path to read from
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to the boundaries in the specified file
      * @throws IllegalArgumentException if no read handler has been registered with \
                the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#boundaries(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static Stream<PlaneConvexSubset> boundaries(final Path path, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static Stream<PlaneConvexSubset> boundaries(final Path path, final \
Precision.DoubleEquivalence precision) {  return boundaries(new \
FileGeometryInput(path), null, precision);  }
 
@@ -218,20 +230,22 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param url URL to read from
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to the boundaries in the specified URL
      * @throws IllegalArgumentException if no read handler has been registered with \
                the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#boundaries(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static Stream<PlaneConvexSubset> boundaries(final URL url, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static Stream<PlaneConvexSubset> boundaries(final URL url, final \
Precision.DoubleEquivalence precision) {  return boundaries(new \
UrlGeometryInput(url), null, precision);  }
 
@@ -243,9 +257,12 @@ public final class IO3D {
      *      // access stream content
      *  }
      *  </pre>
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param in input to read boundaries from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
name} @@ -253,11 +270,12 @@ public final class IO3D {
      * @return stream providing access to the boundaries in the input
      * @throws IllegalArgumentException if no read handler is registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#boundaries(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
     public static Stream<PlaneConvexSubset> boundaries(final GeometryInput in, final \
                GeometryFormat fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return getDefaultManager().boundaries(in, fmt, precision);
     }
 
@@ -273,20 +291,22 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param path file path to read from
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to the triangles in the specified file
      * @throws IllegalArgumentException if no read handler is registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#triangles(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static Stream<Triangle3D> triangles(final Path path, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static Stream<Triangle3D> triangles(final Path path, final \
Precision.DoubleEquivalence precision) {  return triangles(new \
FileGeometryInput(path), null, precision);  }
 
@@ -302,20 +322,22 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     *
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param url URL to read from
      * @param precision precision context used for floating point comparisons
      * @return stream providing access to the triangles from the specified URL
      * @throws IllegalArgumentException if no read handler is registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#triangles(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static Stream<Triangle3D> triangles(final URL url, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static Stream<Triangle3D> triangles(final URL url, final \
Precision.DoubleEquivalence precision) {  return triangles(new UrlGeometryInput(url), \
null, precision);  }
 
@@ -327,9 +349,12 @@ public final class IO3D {
      *      // access stream content
      *  }
      * </pre>
-     * <p>An {@link IOException} is thrown immediately by this method if stream \
                creation fails. Any IO errors
-     * occurring during stream iteration are wrapped with {@link \
                java.io.UncheckedIOException}. Other runtime
-     * exceptions may be thrown during stream iteration if mathematically invalid \
boundaries are encountered.</p> +     * <p>The following exceptions may be thrown \
during stream iteration: +     *  <ul>
+     *      <li>{@link IllegalArgumentException} if mathematically invalid data is \
encountered</li> +     *      <li>{@link IllegalStateException} if a data format \
error occurs</li> +     *      <li>{@link java.io.UncheckedIOException \
UncheckedIOException} if an I/O error occurs</li> +     *  </ul>
      * @param in input to read from
      * @param fmt format of the input; if null, the format is determined implicitly \
                from the
      *      file extension of the input {@link GeometryInput#getFileName() file \
name} @@ -337,11 +362,12 @@ public final class IO3D {
      * @return stream providing access to the triangles in the input
      * @throws IllegalArgumentException if no read handler is registered with the
      *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if stream creation fails
+     * @throws IllegalStateException if a data format error occurs during stream \
creation +     * @throws java.io.UncheckedIOException if an I/O error occurs during \
                stream creation
      * @see BoundaryIOManager3D#triangles(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
     public static Stream<Triangle3D> triangles(final GeometryInput in, final \
                GeometryFormat fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return getDefaultManager().triangles(in, fmt, precision);
     }
 
@@ -351,13 +377,13 @@ public final class IO3D {
      * @param path file path to read from
      * @param precision precision context used for floating point comparisons
      * @return object containing all boundaries from the file at the given path
-     * @throws IllegalArgumentException if no read handler is registered with the
-     *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read handler +     *      is registered with the {@link \
#getDefaultManager() default manager} for the input format +     * @throws \
IllegalStateException if a data format error occurs +     * @throws \
                java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#read(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static BoundarySource3D read(final Path path, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static BoundarySource3D read(final Path path, final \
Precision.DoubleEquivalence precision) {  return read(new FileGeometryInput(path), \
null, precision);  }
 
@@ -367,13 +393,13 @@ public final class IO3D {
      * @param url URL to read from
      * @param precision precision context used for floating point comparisons
      * @return object containing all boundaries from the given URL
-     * @throws IllegalArgumentException if no read handler is registered with the
-     *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read handler +     *      is registered with the {@link \
#getDefaultManager() default manager} for the input format +     * @throws \
IllegalStateException if a data format error occurs +     * @throws \
                java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#read(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static BoundarySource3D read(final URL url, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static BoundarySource3D read(final URL url, final \
Precision.DoubleEquivalence precision) {  return read(new UrlGeometryInput(url), \
null, precision);  }
 
@@ -384,13 +410,14 @@ public final class IO3D {
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @param precision precision context used for floating point comparisons
      * @return object containing all boundaries from the input
-     * @throws IllegalArgumentException if no read handler is registered with the
-     *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read handler +     *      is registered with the {@link \
#getDefaultManager() default manager} for the input format +     * @throws \
IllegalStateException if a data format error occurs +     * @throws \
                java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#read(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
     public static BoundarySource3D read(final GeometryInput in, final GeometryFormat \
                fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return getDefaultManager().read(in, fmt, precision);
     }
 
@@ -400,13 +427,13 @@ public final class IO3D {
      * @param path file path to read from
      * @param precision precision context used for floating point comparisons
      * @return mesh containing all triangles from the given file path
-     * @throws IllegalArgumentException if no read handler is registered with the
-     *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read handler +     *      is registered with the {@link \
#getDefaultManager() default manager} for the input format +     * @throws \
IllegalStateException if a data format error occurs +     * @throws \
                java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#readTriangleMesh(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static TriangleMesh readTriangleMesh(final Path path, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static TriangleMesh readTriangleMesh(final Path path, final \
                Precision.DoubleEquivalence precision) {
         return readTriangleMesh(new FileGeometryInput(path), null, precision);
     }
 
@@ -416,13 +443,13 @@ public final class IO3D {
      * @param url URL to read from
      * @param precision precision context used for floating point comparisons
      * @return mesh containing all triangles from the given URL
-     * @throws IllegalArgumentException if no read handler is registered with the
-     *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read handler +     *      is registered with the {@link \
#getDefaultManager() default manager} for the input format +     * @throws \
IllegalStateException if a data format error occurs +     * @throws \
                java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#readTriangleMesh(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
-    public static TriangleMesh readTriangleMesh(final URL url, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public static TriangleMesh readTriangleMesh(final URL url, final \
                Precision.DoubleEquivalence precision) {
         return readTriangleMesh(new UrlGeometryInput(url), null, precision);
     }
 
@@ -433,13 +460,14 @@ public final class IO3D {
      *      file extension of the input {@link GeometryInput#getFileName() file \
                name}
      * @param precision precision context used for floating point comparisons
      * @return a mesh containing all triangles from the input
-     * @throws IllegalArgumentException if no read handler is registered with the
-     *      {@link #getDefaultManager() default manager} for the input format
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalArgumentException if mathematically invalid data is \
encountered or no read handler +     *      is registered with the {@link \
#getDefaultManager() default manager} for the input format +     * @throws \
IllegalStateException if a data format error occurs +     * @throws \
                java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#readTriangleMesh(GeometryInput, GeometryFormat, \
                Precision.DoubleEquivalence)
      */
     public static TriangleMesh readTriangleMesh(final GeometryInput in, final \
                GeometryFormat fmt,
-            final Precision.DoubleEquivalence precision) throws IOException {
+            final Precision.DoubleEquivalence precision) {
         return getDefaultManager().readTriangleMesh(in, fmt, precision);
     }
 
@@ -452,10 +480,10 @@ public final class IO3D {
      * @param path file path to write to
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#write(Stream, GeometryOutput, GeometryFormat)
      */
-    public static void write(final Stream<? extends PlaneConvexSubset> boundaries, \
final Path path) throws IOException { +    public static void write(final Stream<? \
extends PlaneConvexSubset> boundaries, final Path path) {  write(boundaries, new \
FileGeometryOutput(path), null);  }
 
@@ -469,11 +497,11 @@ public final class IO3D {
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#write(Stream, GeometryOutput, GeometryFormat)
      */
     public static void write(final Stream<? extends PlaneConvexSubset> boundaries, \
                final GeometryOutput out,
-            final GeometryFormat fmt) throws IOException {
+            final GeometryFormat fmt) {
         getDefaultManager().write(boundaries, out, fmt);
     }
 
@@ -484,12 +512,11 @@ public final class IO3D {
      * @param path file path to write to
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see org.apache.commons.geometry.io.core.BoundaryIOManager#write(
      *      org.apache.commons.geometry.core.partitioning.BoundarySource, \
                GeometryOutput, GeometryFormat)
      */
-    public static void write(final BoundarySource3D src, final Path path)
-            throws IOException {
+    public static void write(final BoundarySource3D src, final Path path) {
         write(src, new FileGeometryOutput(path), null);
     }
 
@@ -500,12 +527,11 @@ public final class IO3D {
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see org.apache.commons.geometry.io.core.BoundaryIOManager#write(
      *      org.apache.commons.geometry.core.partitioning.BoundarySource, \
                GeometryOutput, GeometryFormat)
      */
-    public static void write(final BoundarySource3D src, final GeometryOutput out, \
                final GeometryFormat fmt)
-            throws IOException {
+    public static void write(final BoundarySource3D src, final GeometryOutput out, \
final GeometryFormat fmt) {  getDefaultManager().write(src, out, fmt);
     }
 
@@ -515,11 +541,10 @@ public final class IO3D {
      * @param path path to write to
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#writeFacets(Collection, GeometryOutput, \
                GeometryFormat)
      */
-    public static void writeFacets(final Collection<? extends FacetDefinition> \
                facets, final Path path)
-            throws IOException {
+    public static void writeFacets(final Collection<? extends FacetDefinition> \
facets, final Path path) {  writeFacets(facets, new FileGeometryOutput(path), null);
     }
 
@@ -530,11 +555,11 @@ public final class IO3D {
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#writeFacets(Collection, GeometryOutput, \
                GeometryFormat)
      */
     public static void writeFacets(final Collection<? extends FacetDefinition> \
                facets, final GeometryOutput out,
-            final GeometryFormat fmt) throws IOException {
+            final GeometryFormat fmt) {
         getDefaultManager().writeFacets(facets, out, fmt);
     }
 
@@ -547,10 +572,10 @@ public final class IO3D {
      * @param path path to write to
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#writeFacets(Stream, GeometryOutput, GeometryFormat)
      */
-    public static void writeFacets(final Stream<? extends FacetDefinition> facets, \
final Path path) throws IOException { +    public static void writeFacets(final \
Stream<? extends FacetDefinition> facets, final Path path) {  writeFacets(facets, new \
FileGeometryOutput(path), null);  }
 
@@ -564,11 +589,11 @@ public final class IO3D {
      *      file extension of the output {@link GeometryOutput#getFileName() file \
                name}
      * @throws IllegalArgumentException if no write handler is registered with the
      *      {@link #getDefaultManager() default manager} for the output format
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see BoundaryIOManager3D#writeFacets(Stream, GeometryOutput, GeometryFormat)
      */
     public static void writeFacets(final Stream<? extends FacetDefinition> facets, \
                final GeometryOutput out,
-            final GeometryFormat fmt) throws IOException {
+            final GeometryFormat fmt) {
         getDefaultManager().writeFacets(facets, out, fmt);
     }
 
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjParser.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjParser.java
 index 4f243c7..73c9d56 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjParser.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjParser.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -53,10 +52,10 @@ public abstract class AbstractObjParser {
      * and false if the end of the content has been reached. Keywords consist of \
                alphanumeric
      * strings placed at the beginning of lines. Comments and blank lines are \
                ignored.
      * @return true if a keyword has been found and false if the end of content has \
                been reached
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if invalid content is found
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public boolean nextKeyword() throws IOException {
+    public boolean nextKeyword() {
         currentKeyword = null;
 
         // advance to the next line if not at the start of a line
@@ -95,9 +94,9 @@ public abstract class AbstractObjParser {
      * account.
      * @return remaining content on the current data line or null if the end of the \
                content has
      *      been reached
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public String readDataLine() throws IOException {
+    public String readDataLine() {
         parser.nextWithLineContinuation(
                 ObjConstants.LINE_CONTINUATION_CHAR,
                 SimpleTextParser::isNotNewLinePart)
@@ -108,9 +107,9 @@ public abstract class AbstractObjParser {
 
     /** Discard remaining content on the current data line, taking line continuation \
                characters into
      * account.
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void discardDataLine() throws IOException {
+    public void discardDataLine() {
         parser.discardWithLineContinuation(
                 ObjConstants.LINE_CONTINUATION_CHAR,
                 SimpleTextParser::isNotNewLinePart)
@@ -119,9 +118,10 @@ public abstract class AbstractObjParser {
 
     /** Read a whitespace-delimited 3D vector from the current data line.
      * @return vector vector read from the current line
-     * @throws IOException if an I/O error occurs
+     * @throws IllegalStateException if parsing fails
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public Vector3D readVector() throws IOException {
+    public Vector3D readVector() {
         discardDataLineWhitespace();
         final double x = nextDouble();
 
@@ -136,10 +136,10 @@ public abstract class AbstractObjParser {
 
     /** Read whitespace-delimited double values from the current data line.
      * @return double values read from the current line
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if double values are not able to be parsed
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public double[] readDoubles() throws IOException {
+    public double[] readDoubles() {
         final List<Double> list = new ArrayList<>();
 
         while (nextDataLineContent()) {
@@ -165,16 +165,16 @@ public abstract class AbstractObjParser {
     /** Method called when a keyword is encountered in the parsed OBJ content. \
                Subclasses should use
      * this method to validate the keyword and/or update any internal state.
      * @param keyword keyword encountered in the OBJ content
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if the given keyword is invalid
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected abstract void handleKeyword(String keyword) throws IOException;
+    protected abstract void handleKeyword(String keyword);
 
     /** Discard whitespace on the current data line, taking line continuation \
                characters into account.
      * @return text parser instance
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected SimpleTextParser discardDataLineWhitespace() throws IOException {
+    protected SimpleTextParser discardDataLineWhitespace() {
         return parser.discardWithLineContinuation(
                 ObjConstants.LINE_CONTINUATION_CHAR,
                 SimpleTextParser::isLineWhitespace);
@@ -183,18 +183,18 @@ public abstract class AbstractObjParser {
     /** Discard whitespace on the current data line and return true if any more \
                characters
      * remain on the line.
      * @return true if more non-whitespace characters remain on the current data \
                line
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected boolean nextDataLineContent() throws IOException {
+    protected boolean nextDataLineContent() {
         return discardDataLineWhitespace().hasMoreCharactersOnLine();
     }
 
     /** Get the next whitespace-delimited double on the current data line.
      * @return the next whitespace-delimited double on the current line
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if a double value is not able to be parsed
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected double nextDouble() throws IOException {
+    protected double nextDouble() {
         return parser.nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR,
                 SimpleTextParser::isNotWhitespace)
             .getCurrentTokenAsDouble();
@@ -203,9 +203,9 @@ public abstract class AbstractObjParser {
     /** Read a keyword consisting of alphanumeric characters from the current parser \
                position and set it
      * as the current token. Returns true if a non-empty keyword was found.
      * @return true if a non-empty keyword was found.
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private boolean readKeyword() throws IOException {
+    private boolean readKeyword() {
         return parser
                 .nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, \
                SimpleTextParser::isAlphanumeric)
                 .hasNonEmptyToken();
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjPolygonReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjPolygonReader.java
 index 9669e05..55612c0 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjPolygonReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/AbstractObjPolygonReader.java
 @@ -17,10 +17,10 @@
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
 import java.io.Closeable;
-import java.io.IOException;
 import java.io.Reader;
 
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 
 /** Abstract base class for types that read OBJ polygon content using
  * {@link PolygonObjParser}.
@@ -41,10 +41,10 @@ public abstract class AbstractObjPolygonReader implements \
Closeable {  this.parser = new PolygonObjParser(reader);
     }
 
-    /** Get the flag indicating whether or not an {@link IOException} will be thrown
+    /** Get the flag indicating whether or not an {@link IllegalStateException} will \
                be thrown
      * if the OBJ content contains any keywords defining non-polygon geometric \
                content
      * (ex: {@code curv}). If false, non-polygon data is ignored.
-     * @return flag indicating whether or not an {@link IOException} will be thrown
+     * @return flag indicating whether or not an {@link IllegalStateException} will \
                be thrown
      *      if non-polygon content is encountered
      * @see PolygonObjParser#getFailOnNonPolygonKeywords()
      */
@@ -52,10 +52,10 @@ public abstract class AbstractObjPolygonReader implements \
Closeable {  return parser.getFailOnNonPolygonKeywords();
     }
 
-    /** Set the flag indicating whether or not an {@link IOException} will be thrown
+    /** Set the flag indicating whether or not an {@link IllegalStateException} will \
                be thrown
      * if the OBJ content contains any keywords defining non-polygon geometric \
                content
      * (ex: {@code curv}). If set to false, non-polygon data is ignored.
-     * @param fail flag indicating whether or not an {@link IOException} will be \
thrown +     * @param fail flag indicating whether or not an {@link \
                IllegalStateException} will be thrown
      *      if non-polygon content is encountered
      */
     public void setFailOnNonPolygonKeywords(final boolean fail) {
@@ -64,15 +64,16 @@ public abstract class AbstractObjPolygonReader implements \
Closeable {  
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
-        reader.close();
+    public void close() {
+        GeometryIOUtils.closeUnchecked(reader);
     }
 
     /** Return the next face from the OBJ content or null if no face is found.
      * @return the next face from the OBJ content or null if no face is found
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a parsing error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected PolygonObjParser.Face readFace() throws IOException {
+    protected PolygonObjParser.Face readFace() {
         while (parser.nextKeyword()) {
             switch (parser.getCurrentKeyword()) {
             case ObjConstants.VERTEX_KEYWORD:
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3D.java
 index 830b305..f4e950d 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3D.java
 @@ -16,9 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -26,6 +23,7 @@ import java.nio.charset.StandardCharsets;
 import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
 import org.apache.commons.geometry.io.core.GeometryFormat;
 import org.apache.commons.geometry.io.core.input.GeometryInput;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.euclidean.threed.AbstractBoundaryReadHandler3D;
  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
 import org.apache.commons.geometry.io.euclidean.threed.GeometryFormat3D;
@@ -63,29 +61,24 @@ public class ObjBoundaryReadHandler3D extends \
AbstractBoundaryReadHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public FacetDefinitionReader facetDefinitionReader(final GeometryInput in) \
throws IOException { +    public FacetDefinitionReader facetDefinitionReader(final \
GeometryInput in) {  return new ObjFacetDefinitionReader(createReader(in));
     }
 
     /** {@inheritDoc} */
     @Override
-    public TriangleMesh readTriangleMesh(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-            throws IOException {
+    public TriangleMesh readTriangleMesh(final GeometryInput in, final \
                Precision.DoubleEquivalence precision) {
         try (ObjTriangleMeshReader meshReader = new \
ObjTriangleMeshReader(createReader(in), precision)) {  return \
meshReader.readTriangleMesh();  }
     }
 
-    /** Creat a {@link Reader} for reading character data from the given input.
+    /** Create a {@link Reader} for reading character data from the given input.
      * @param in input to read from
      * @return reader instance
-     * @throws IOException if an IO error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private Reader createReader(final GeometryInput in) throws IOException {
-        final Charset charset = in.getCharset() != null ?
-                in.getCharset() :
-                defaultCharset;
-
-        return new BufferedReader(new InputStreamReader(in.getInputStream(), \
charset)); +    private Reader createReader(final GeometryInput in) {
+        return GeometryIOUtils.createBufferedReader(in, defaultCharset);
     }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3D.java
 index b9d3f60..dedf3bb 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3D.java
 @@ -16,9 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.nio.charset.Charset;
 import java.util.Iterator;
 import java.util.function.DoubleFunction;
@@ -28,6 +25,7 @@ import \
org.apache.commons.geometry.euclidean.threed.BoundarySource3D;  import \
org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;  import \
org.apache.commons.geometry.euclidean.threed.mesh.Mesh;  import \
org.apache.commons.geometry.io.core.GeometryFormat; +import \
org.apache.commons.geometry.io.core.internal.GeometryIOUtils;  import \
org.apache.commons.geometry.io.core.output.GeometryOutput;  import \
org.apache.commons.geometry.io.euclidean.threed.AbstractBoundaryWriteHandler3D;  \
import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition; @@ -129,8 \
+127,7 @@ public class ObjBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public void write(final BoundarySource3D src, final GeometryOutput out)
-            throws IOException {
+    public void write(final BoundarySource3D src, final GeometryOutput out) {
         // write meshes directly instead of iterating through boundaries
         if (src instanceof Mesh) {
             try (ObjWriter writer = createWriter(out)) {
@@ -143,8 +140,7 @@ public class ObjBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public void write(final Stream<? extends PlaneConvexSubset> boundaries, final \
                GeometryOutput out)
-            throws IOException {
+    public void write(final Stream<? extends PlaneConvexSubset> boundaries, final \
GeometryOutput out) {  try (ObjWriter writer = createWriter(out)) {
             final ObjWriter.MeshBuffer meshBuffer = \
writer.meshBuffer(meshBufferBatchSize);  
@@ -159,8 +155,7 @@ public class ObjBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
                GeometryOutput out)
-            throws IOException {
+    public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
GeometryOutput out) {  try (ObjWriter writer = createWriter(out)) {
             final ObjWriter.MeshBuffer meshBuffer = \
writer.meshBuffer(meshBufferBatchSize);  
@@ -177,14 +172,10 @@ public class ObjBoundaryWriteHandler3D extends \
                AbstractBoundaryWriteHandler3D {
      * output stream.
      * @param out output stream to write to
      * @return new {@code OBJWriter} for writing content to the given output stream
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private ObjWriter createWriter(final GeometryOutput out) throws IOException {
-        final Charset charset = out.getCharset() != null ?
-                out.getCharset() :
-                defaultCharset;
-
-        final ObjWriter writer =
-                new ObjWriter(new BufferedWriter(new \
OutputStreamWriter(out.getOutputStream(), charset))); +    private ObjWriter \
createWriter(final GeometryOutput out) { +        final ObjWriter writer = new \
ObjWriter(GeometryIOUtils.createBufferedWriter(out, defaultCharset));  \
writer.setLineSeparator(lineSeparator);  writer.setDoubleFormat(doubleFormat);
 
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReader.java
 index 2347cda..1e6c0f0 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReader.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.List;
@@ -46,7 +45,7 @@ public class ObjFacetDefinitionReader extends \
AbstractObjPolygonReader  
     /** {@inheritDoc} */
     @Override
-    public FacetDefinition readFacet() throws IOException {
+    public FacetDefinition readFacet() {
         final PolygonObjParser.Face face = readFace();
         if (face != null) {
             final List<Vector3D> faceVertices = face.getVertices(vertices::get);
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReader.java
 index f1638c0..aeb40d6 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReader.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -52,9 +51,10 @@ public class ObjTriangleMeshReader extends \
                AbstractObjPolygonReader {
      * triangle fan. All vertices present in the OBJ content are also present in the \
                returned mesh,
      * regardless of whether or not they are used in a face.
      * @return triangle mesh containing all data from the OBJ content
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public TriangleMesh readTriangleMesh() throws IOException {
+    public TriangleMesh readTriangleMesh() {
         PolygonObjParser.Face face;
         Vector3D definedNormal;
         Iterator<PolygonObjParser.VertexAttributes> attrs;
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriter.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriter.java
 index ff9292b..d7472e9 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriter.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriter.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -71,9 +70,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
 
     /** Write an OBJ comment with the given value.
      * @param comment comment to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeComment(final String comment) throws IOException {
+    public void writeComment(final String comment) {
         for (final String line : comment.split("\\R")) {
             write(ObjConstants.COMMENT_CHAR);
             write(SPACE);
@@ -86,9 +85,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      * does not affect the geometry, although it may affect how the file content
      * is read by other programs.
      * @param objectName the name to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeObjectName(final String objectName) throws IOException {
+    public void writeObjectName(final String objectName) {
         writeKeywordLine(ObjConstants.OBJECT_KEYWORD, objectName);
     }
 
@@ -96,27 +95,27 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      * does not affect the geometry, although it may affect how the file content
      * is read by other programs.
      * @param groupName the name to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeGroupName(final String groupName) throws IOException {
+    public void writeGroupName(final String groupName) {
         writeKeywordLine(ObjConstants.GROUP_KEYWORD, groupName);
     }
 
     /** Write a vertex and return the 0-based index of the vertex in the output.
      * @param vertex vertex to write
-     * @throws IOException if an I/O error occurs
      * @return 0-based index of the written vertex
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public int writeVertex(final Vector3D vertex) throws IOException {
+    public int writeVertex(final Vector3D vertex) {
         return writeVertexLine(createVectorString(vertex));
     }
 
     /** Write a vertex normal and return the 0-based index of the normal in the \
                output.
      * @param normal normal to write
-     * @throws IOException if an I/O error occurs
      * @return 0-based index of the written normal
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public int writeVertexNormal(final Vector3D normal) throws IOException {
+    public int writeVertexNormal(final Vector3D normal) {
         return writeVertexNormalLine(createVectorString(normal));
     }
 
@@ -125,9 +124,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      * @throws IllegalArgumentException if fewer than 3 vertex indices are given
      * @throws IndexOutOfBoundsException if any vertex index is computed to be \
                outside of
      *      the bounds of the elements written so far
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeFace(final int... vertexIndices) throws IOException {
+    public void writeFace(final int... vertexIndices) {
         writeFaceWithOffsets(0, vertexIndices, 0, null);
     }
 
@@ -137,9 +136,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      * @param normalIndex 0-based normal index
      * @throws IndexOutOfBoundsException if any vertex or normal index is computed \
                to be outside of
      *      the bounds of the elements written so far
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeFace(final int[] vertexIndices, final int normalIndex) throws \
IOException { +    public void writeFace(final int[] vertexIndices, final int \
normalIndex) {  final int[] normalIndices = new int[vertexIndices.length];
         Arrays.fill(normalIndices, normalIndex);
 
@@ -156,9 +155,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      *      is not null but has a different length than {@code vertexIndices}
      * @throws IndexOutOfBoundsException if any vertex or normal index is computed \
                to be outside of
      *      the bounds of the elements written so far
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeFace(final int[] vertexIndices, final int[] normalIndices) \
throws IOException { +    public void writeFace(final int[] vertexIndices, final \
int[] normalIndices) {  writeFaceWithOffsets(0, vertexIndices, 0, normalIndices);
     }
 
@@ -166,11 +165,11 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      * with an unlimited size.
      * @param src boundary source containing the boundaries to write to the output
      * @throws IllegalArgumentException if any boundary in the argument is infinite
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #meshBuffer(int)
      * @see #writeMesh(Mesh)
      */
-    public void writeBoundaries(final BoundarySource3D src) throws IOException {
+    public void writeBoundaries(final BoundarySource3D src) {
         writeBoundaries(src, -1);
     }
 
@@ -180,12 +179,11 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      * @param batchSize batch size to use for the mesh buffer; pass {@code -1} to \
                use a buffer
      *      of unlimited size
      * @throws IllegalArgumentException if any boundary in the argument is infinite
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #meshBuffer(int)
      * @see #writeMesh(Mesh)
      */
-    public void writeBoundaries(final BoundarySource3D src, final int batchSize)
-            throws IOException {
+    public void writeBoundaries(final BoundarySource3D src, final int batchSize) {
         final MeshBuffer buffer = meshBuffer(batchSize);
 
         try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
@@ -201,9 +199,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
     /** Write a mesh to the output. All vertices and faces are written exactly as \
                found. For example,
      * if a vertex is duplicated in the argument, it will also be duplicated in the \
                output.
      * @param mesh the mesh to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeMesh(final Mesh<?> mesh) throws IOException {
+    public void writeMesh(final Mesh<?> mesh) {
         final int vertexOffset = vertexCount;
 
         for (final Vector3D vertex : mesh.vertices()) {
@@ -249,10 +247,10 @@ public final class ObjWriter extends AbstractTextFormatWriter {
      *      is not null but has a different length than {@code vertexIndices}
      * @throws IndexOutOfBoundsException if any vertex or normal index is computed \
                to be outside of
      *      the bounds of the elements written so far
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     private void writeFaceWithOffsets(final int vertexOffset, final int[] \
                vertexIndices,
-            final int normalOffset, final int[] normalIndices) throws IOException {
+            final int normalOffset, final int[] normalIndices) {
         if (vertexIndices.length < 3) {
             throw new IllegalArgumentException("Face must have more than 3 vertices; \
                found " + vertexIndices.length);
         } else if (normalIndices != null && normalIndices.length != \
vertexIndices.length) { @@ -310,9 +308,9 @@ public final class ObjWriter extends \
AbstractTextFormatWriter {  /** Write a vertex line containing the given string \
                content.
      * @param content vertex string content
      * @return the 0-based index of the added vertex
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private int writeVertexLine(final String content) throws IOException {
+    private int writeVertexLine(final String content) {
         writeKeywordLine(ObjConstants.VERTEX_KEYWORD, content);
         return vertexCount++;
     }
@@ -320,9 +318,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
     /** Write a vertex normal line containing the given string content.
      * @param content vertex normal string content
      * @return the 0-based index of the added vertex normal
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private int writeVertexNormalLine(final String content) throws IOException {
+    private int writeVertexNormalLine(final String content) {
         writeKeywordLine(ObjConstants.VERTEX_NORMAL_KEYWORD, content);
         return normalCount++;
     }
@@ -330,9 +328,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
     /** Write a line of content prefixed with the given OBJ keyword.
      * @param keyword OBJ keyword
      * @param content line content
-     * @throws IOException if and I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void writeKeywordLine(final String keyword, final String content) throws \
IOException { +    private void writeKeywordLine(final String keyword, final String \
content) {  write(keyword);
         write(SPACE);
         write(content);
@@ -382,9 +380,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
          * of currently stored faces is greater than or equal to {@code batchSize}, \
                then the buffer
          * content is written to the output and the buffer state is reset.
          * @param facet facet to add
-         * @throws IOException if an I/O error occurs
+         * @throws java.io.UncheckedIOException if an I/O error occurs
          */
-        public void add(final FacetDefinition facet) throws IOException {
+        public void add(final FacetDefinition facet) {
             addFace(facet.getVertices(), facet.getNormal());
         }
 
@@ -393,9 +391,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
          * content is written to the output and the buffer state is reset.
          * @param boundary boundary to add
          * @throws IllegalArgumentException if the boundary is infinite
-         * @throws IOException if an I/O error occurs
+         * @throws java.io.UncheckedIOException if an I/O error occurs
          */
-        public void add(final PlaneConvexSubset boundary) throws IOException {
+        public void add(final PlaneConvexSubset boundary) {
             if (boundary.isInfinite()) {
                 throw new IllegalArgumentException("OBJ input geometry cannot be \
infinite: " + boundary);  } else if (!boundary.isEmpty()) {
@@ -420,9 +418,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
         }
 
         /** Flush the buffer content to the output and reset its state.
-         * @throws IOException if an I/O error occurs
+         * @throws java.io.UncheckedIOException if an I/O error occurs
          */
-        public void flush() throws IOException {
+        public void flush() {
             final int vertexOffset = vertexCount;
             final int normalOffset = normalCount;
 
@@ -474,9 +472,9 @@ public final class ObjWriter extends AbstractTextFormatWriter {
          * content is written to the output and the buffer state is reset.
          * @param vertices face vertices
          * @param normal face normal; may be null
-         * @throws IOException if an I/O error occurs
+         * @throws java.io.UncheckedIOException if an I/O error occurs
          */
-        private void addFace(final List<Vector3D> vertices, final Vector3D normal) \
throws IOException { +        private void addFace(final List<Vector3D> vertices, \
final Vector3D normal) {  final int faceIndex = faceVertices.size();
 
             final int[] vertexIndices = new int[vertices.size()];
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
 index 5b7b452..4eff147 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -105,8 +104,8 @@ public class PolygonObjParser extends AbstractObjParser {
         return textureCoordinateCount;
     }
 
-    /** Return true if the instance is configured to throw an {@link IOException} \
                when OBJ keywords not commonly
-     * used with files containing only polygon data are encountered. The default \
value is {@code false}, +    /** Return true if the instance is configured to throw \
an {@link IllegalStateException} when OBJ keywords +     * not commonly used with \
files containing only polygon data are encountered. The default value is {@code \
                false},
      * meaning that no keyword validation is performed. When set to true, only the \
                following keywords are
      * accepted:
      * <ul>
@@ -126,9 +125,9 @@ public class PolygonObjParser extends AbstractObjParser {
         return failOnNonPolygonKeywords;
     }
 
-    /** Set the flag determining if the instance should throw an {@link IOException} \
                when encountering keywords
-     * not commonly used with OBJ files containing only polygon data. If true, only \
                the following keywords are
-     * accepted:
+    /** Set the flag determining if the instance should throw an {@link \
IllegalStateException} when encountering +     * keywords not commonly used with OBJ \
files containing only polygon data. If true, only the following keywords +     * are \
                accepted:
      * <ul>
      *  <li>{@code v}</li>
      *  <li>{@code vn}</li>
@@ -149,7 +148,7 @@ public class PolygonObjParser extends AbstractObjParser {
 
     /** {@inheritDoc} */
     @Override
-    protected void handleKeyword(final String keywordValue) throws IOException {
+    protected void handleKeyword(final String keywordValue) {
         if (failOnNonPolygonKeywords && \
                !STANDARD_POLYGON_KEYWORDS.contains(keywordValue)) {
             final String allowedKeywords = STANDARD_POLYGON_KEYWORDS.stream()
                     .sorted()
@@ -177,10 +176,10 @@ public class PolygonObjParser extends AbstractObjParser {
 
     /** Read an OBJ face definition from the current line.
      * @return OBJ face definition read from the current line
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if a face definition is not able to be parsed
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public Face readFace() throws IOException {
+    public Face readFace() {
         final List<VertexAttributes> vertices = new ArrayList<>();
 
         while (nextDataLineContent()) {
@@ -199,10 +198,10 @@ public class PolygonObjParser extends AbstractObjParser {
 
     /** Read an OBJ face vertex definition from the current parser position.
      * @return OBJ face vertex definition
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if a vertex definition is not able to be parsed
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private VertexAttributes readFaceVertex() throws IOException {
+    private VertexAttributes readFaceVertex() {
         final SimpleTextParser parser = getTextParser();
 
         discardDataLineWhitespace();
@@ -236,11 +235,11 @@ public class PolygonObjParser extends AbstractObjParser {
      * @param available number of available values of the given type parsed from the \
                content
      *      so far
      * @return 0-based positive attribute index
-     * @throws IOException if an I/O error occurs
      * @throws IllegalStateException if the integer index cannot be parsed or the \
                index is
      *      out of range for the number of parsed elements of the given type
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private int readNormalizedVertexAttributeIndex(final String type, final int \
available) throws IOException { +    private int \
readNormalizedVertexAttributeIndex(final String type, final int available) {  final \
SimpleTextParser parser = getTextParser();  
         final int objIndex = parser
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReader.java
 index c953eb5..7d3d124 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReader.java
 @@ -16,13 +16,13 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.util.Arrays;
 
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
 
 /** Class used to read the binary form of the STL file format.
@@ -58,9 +58,9 @@ public class BinaryStlFacetDefinitionReader implements \
                FacetDefinitionReader {
     /** Get a read-only buffer containing the 80 bytes of the STL header. The header \
                does not
      * include the 4-byte value indicating the total number of triangles in the STL \
                file.
      * @return the STL header content
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public ByteBuffer getHeader() throws IOException {
+    public ByteBuffer getHeader() {
         beginRead();
         return ByteBuffer.wrap(header.array().clone());
     }
@@ -68,9 +68,9 @@ public class BinaryStlFacetDefinitionReader implements \
                FacetDefinitionReader {
     /** Return the header content as a string decoded using the UTF-8 charset. \
                Control
      * characters (such as '\0') are not included in the result.
      * @return the header content decoded as a UTF-8 string
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public String getHeaderAsString() throws IOException {
+    public String getHeaderAsString() {
         return getHeaderAsString(StlConstants.DEFAULT_CHARSET);
     }
 
@@ -78,9 +78,9 @@ public class BinaryStlFacetDefinitionReader implements \
                FacetDefinitionReader {
      * characters (such as '\0') are not included in the result.
      * @param charset charset to decode the header with
      * @return the header content decoded as a string
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public String getHeaderAsString(final Charset charset) throws IOException {
+    public String getHeaderAsString(final Charset charset) {
         // decode the entire header as characters in the given charset
         final String raw = charset.decode(getHeader()).toString();
 
@@ -97,16 +97,16 @@ public class BinaryStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Get the total number of triangles (i.e. facets) declared to be present in \
                the input.
      * @return total number of triangle in the input
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public long getNumTriangles() throws IOException {
+    public long getNumTriangles() {
         beginRead();
         return triangleTotal;
     }
 
     /** {@inheritDoc} */
     @Override
-    public BinaryStlFacetDefinition readFacet() throws IOException {
+    public BinaryStlFacetDefinition readFacet() {
         beginRead();
 
         BinaryStlFacetDefinition facet = null;
@@ -122,19 +122,18 @@ public class BinaryStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
-        in.close();
+    public void close() {
+        GeometryIOUtils.closeUnchecked(in);
     }
 
     /** Read the file header content and triangle count.
-     * @throws IOException if an I/O error occurs
+     * @throws IllegalStateException is a parse error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void beginRead() throws IOException {
+    private void beginRead() {
         if (!hasReadHeader) {
             // read header content
-            final int headerBytesRead =
-                    in.read(header.array(), 0, StlConstants.BINARY_HEADER_BYTES);
-
+            final int headerBytesRead = \
GeometryIOUtils.applyAsIntUnchecked(in::read, header.array());  if (headerBytesRead < \
StlConstants.BINARY_HEADER_BYTES) {  throw dataNotAvailable("header");
             }
@@ -157,7 +156,7 @@ public class BinaryStlFacetDefinitionReader implements \
FacetDefinitionReader {  /** Internal method to read a single facet from the input.
      * @return facet read from the input
      */
-    private BinaryStlFacetDefinition readFacetInternal() throws IOException {
+    private BinaryStlFacetDefinition readFacetInternal() {
         if (fill(triangleBuffer) < triangleBuffer.capacity()) {
             throw dataNotAvailable("triangle at index " + trianglesRead);
         }
@@ -176,10 +175,10 @@ public class BinaryStlFacetDefinitionReader implements \
                FacetDefinitionReader {
      * made ready for reading.
      * @param buf buffer to fill
      * @return number of bytes read
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private int fill(final ByteBuffer buf) throws IOException {
-        int read = in.read(buf.array());
+    private int fill(final ByteBuffer buf) {
+        int read = GeometryIOUtils.applyAsIntUnchecked(in::read, buf.array());
         buf.rewind();
 
         return read;
@@ -198,12 +197,12 @@ public class BinaryStlFacetDefinitionReader implements \
FacetDefinitionReader {  return Vector3D.of(x, y, z);
     }
 
-    /** Return an IOException stating that data is not available for the file
+    /** Return an exception stating that data is not available for the file
      * component with the given name.
      * @param name name of the file component missing data
      * @return exception instance
      */
-    private static IOException dataNotAvailable(final String name) {
-        return new IOException("Failed to read STL " + name + ": data not \
available"); +    private static IllegalStateException dataNotAvailable(final String \
name) { +        return GeometryIOUtils.parseError("Failed to read STL " + name + ": \
data not available");  }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriter.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriter.java
 index 73ce9e9..123f8df 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriter.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriter.java
 @@ -17,11 +17,11 @@
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
 import java.io.Closeable;
-import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
 
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 
 /** Low-level class for writing binary STL content.
  */
@@ -45,9 +45,9 @@ public class BinaryStlWriter implements Closeable {
      * are written to the header, with any remaining bytes of the header filled with \
                zeros.
      * @param headerContent bytes to include in the header; may be null
      * @param triangleCount number of triangles to be included in the content
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeHeader(final byte[] headerContent, final int triangleCount) \
throws IOException { +    public void writeHeader(final byte[] headerContent, final \
int triangleCount) {  writeHeader(headerContent, triangleCount, out);
     }
 
@@ -63,10 +63,10 @@ public class BinaryStlWriter implements Closeable {
      * @param p2 second point
      * @param p3 third point
      * @param normal triangle normal; may be null
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D \
                p3,
-            final Vector3D normal) throws IOException {
+            final Vector3D normal) {
         writeTriangle(p1, p2, p3, normal, 0);
     }
 
@@ -84,10 +84,10 @@ public class BinaryStlWriter implements Closeable {
      * @param p3 third point
      * @param normal triangle normal; may be null
      * @param attributeValue 2-byte STL triangle attribute value
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
     public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D \
                p3,
-            final Vector3D normal, final int attributeValue) throws IOException {
+            final Vector3D normal, final int attributeValue) {
         triangleBuffer.rewind();
 
         putVector(StlUtils.determineNormal(p1, p2, p3, normal));
@@ -103,13 +103,13 @@ public class BinaryStlWriter implements Closeable {
 
         triangleBuffer.putShort((short) attributeValue);
 
-        out.write(triangleBuffer.array());
+        GeometryIOUtils.acceptUnchecked(out::write, triangleBuffer.array());
     }
 
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
-        out.close();
+    public void close() {
+        GeometryIOUtils.closeUnchecked(out);
     }
 
     /** Put all double components of {@code vec} into the internal buffer.
@@ -128,10 +128,9 @@ public class BinaryStlWriter implements Closeable {
      * @param headerContent
      * @param triangleCount
      * @param out
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    static void writeHeader(final byte[] headerContent, final int triangleCount, \
                final OutputStream out)
-            throws IOException {
-
+    static void writeHeader(final byte[] headerContent, final int triangleCount, \
final OutputStream out) {  // write the header
         final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES];
         if (headerContent != null) {
@@ -141,13 +140,13 @@ public class BinaryStlWriter implements Closeable {
                     Math.min(headerContent.length, \
StlConstants.BINARY_HEADER_BYTES));  }
 
-        out.write(bytes);
+        GeometryIOUtils.acceptUnchecked(out::write, bytes);
 
         // write the triangle count number
         ByteBuffer countBuffer = StlUtils.byteBuffer(Integer.BYTES);
         countBuffer.putInt(triangleCount);
         countBuffer.flip();
 
-        out.write(countBuffer.array());
+        GeometryIOUtils.acceptUnchecked(out::write, countBuffer.array());
     }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3D.java
 index a137790..1114e0b 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3D.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
-import java.io.IOException;
 import java.nio.charset.Charset;
 
 import org.apache.commons.geometry.io.core.GeometryFormat;
@@ -58,7 +57,7 @@ public class StlBoundaryReadHandler3D extends \
AbstractBoundaryReadHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public FacetDefinitionReader facetDefinitionReader(final GeometryInput in) \
throws IOException { +    public FacetDefinitionReader facetDefinitionReader(final \
GeometryInput in) {  final Charset inputCharset = in.getCharset() != null ?
                 in.getCharset() :
                 defaultCharset;
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java
 index f785cac..47888c5 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java
 @@ -30,6 +30,7 @@ import org.apache.commons.geometry.euclidean.threed.Triangle3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
 import org.apache.commons.geometry.io.core.GeometryFormat;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.core.output.GeometryOutput;
 import org.apache.commons.geometry.io.euclidean.threed.AbstractBoundaryWriteHandler3D;
  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
@@ -85,8 +86,7 @@ public class StlBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public void write(final BoundarySource3D src, final GeometryOutput out)
-            throws IOException {
+    public void write(final BoundarySource3D src, final GeometryOutput out) {
         // handle cases where we know the number of triangles to be written up front
         // and do not need to buffer the content
         if (src instanceof TriangleMesh) {
@@ -99,8 +99,7 @@ public class StlBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public void write(final Stream<? extends PlaneConvexSubset> boundaries, \
                GeometryOutput out)
-            throws IOException {
+    public void write(final Stream<? extends PlaneConvexSubset> boundaries, final \
GeometryOutput out) {  
         // write the triangle data to a buffer and track how many we write
         int triangleCount = 0;
@@ -124,16 +123,12 @@ public class StlBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  }
 
         // write the header and copy the data
-        try (OutputStream os = out.getOutputStream()) {
-            BinaryStlWriter.writeHeader(null, triangleCount, os);
-            triangleBuffer.writeTo(os);
-        }
+        writeWithHeader(triangleBuffer, triangleCount, out);
     }
 
     /** {@inheritDoc} */
     @Override
-    public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
                GeometryOutput out)
-            throws IOException {
+    public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
GeometryOutput out) {  
         // write the triangle data to a buffer and track how many we write
         int triangleCount = 0;
@@ -165,9 +160,23 @@ public class StlBoundaryWriteHandler3D extends \
AbstractBoundaryWriteHandler3D {  }
 
         // write the header and copy the data
+        writeWithHeader(triangleBuffer, triangleCount, out);
+    }
+
+    /** Write the given triangle data prefixed with an STL header to the output \
stream from {@code out}. +     * @param triangleBuffer buffer containing STL triangle \
data +     * @param count number of triangles in {@code triangleBuffer}
+     * @param out output to write to
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    private void writeWithHeader(final ByteArrayOutputStream triangleBuffer, final \
int count, +            final GeometryOutput out) {
+        // write the header and copy the data
         try (OutputStream os = out.getOutputStream()) {
-            BinaryStlWriter.writeHeader(null, triangleCount, os);
+            BinaryStlWriter.writeHeader(null, count, os);
             triangleBuffer.writeTo(os);
+        } catch (IOException exc) {
+            throw GeometryIOUtils.createUnchecked(exc);
         }
     }
 
@@ -175,10 +184,9 @@ public class StlBoundaryWriteHandler3D extends \
                AbstractBoundaryWriteHandler3D {
      * format.
      * @param mesh mesh to write
      * @param output output to write to
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void writeTriangleMesh(final TriangleMesh mesh, final GeometryOutput \
                output)
-            throws IOException {
+    private void writeTriangleMesh(final TriangleMesh mesh, final GeometryOutput \
                output) {
         try (BinaryStlWriter stlWriter = new \
BinaryStlWriter(output.getOutputStream())) {  // write the header
             stlWriter.writeHeader(null, mesh.getFaceCount());
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReaders.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReaders.java
 index 601471e..76a2b80 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReaders.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReaders.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
 import java.io.BufferedReader;
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PushbackInputStream;
@@ -25,6 +24,7 @@ import java.nio.charset.Charset;
 import java.text.MessageFormat;
 import java.util.Arrays;
 
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
 
 /** Utility class with factory methods for constructing {@link \
FacetDefinitionReader} @@ -42,10 +42,10 @@ public final class \
                StlFacetDefinitionReaders {
      * @param charset charset to use when checking the input for text content;
      *      if null, the input is assumed to use the UTF-8 charset
      * @return facet definition reader
-     * @throws IOException if an I/O error occurs
+     * @throws IllegalStateException if a parsing error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public static FacetDefinitionReader create(final InputStream in, final Charset \
                charset)
-            throws IOException {
+    public static FacetDefinitionReader create(final InputStream in, final Charset \
charset) {  final Charset inputCharset = charset != null ?
                 charset :
                 StlConstants.DEFAULT_CHARSET;
@@ -53,9 +53,9 @@ public final class StlFacetDefinitionReaders {
         final byte[] testBytes = \
StlConstants.SOLID_START_KEYWORD.getBytes(inputCharset);  final byte[] actualBytes = \
new byte[testBytes.length];  
-        final int read = in.read(actualBytes);
+        final int read = GeometryIOUtils.applyAsIntUnchecked(in::read, actualBytes);
         if (read < actualBytes.length) {
-            throw new IOException(MessageFormat.format(
+            throw GeometryIOUtils.parseError(MessageFormat.format(
                     "Cannot determine STL format: attempted to read {0} bytes but \
found only {1} available",  actualBytes.length, read));
         }
@@ -63,7 +63,7 @@ public final class StlFacetDefinitionReaders {
         // "unread" the test bytes so that the created readers can start from the
         // beginning of the content
         final PushbackInputStream pushbackInput = new PushbackInputStream(in, \
                actualBytes.length);
-        pushbackInput.unread(actualBytes);
+        GeometryIOUtils.acceptUnchecked(pushbackInput::unread, actualBytes);
 
         if (Arrays.equals(testBytes, actualBytes)) {
             // this is a text file
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReader.java
 index c82d8ac..2970ada 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReader.java
 @@ -16,11 +16,11 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.util.Arrays;
 
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
@@ -56,9 +56,10 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Get the name of the STL solid being read or null if no name was specified.
      * @return the name of the STL solid being read or null if no name was specified
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public String getSolidName() throws IOException {
+    public String getSolidName() {
         ensureSolidStarted();
 
         return solidName;
@@ -66,7 +67,7 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** {@inheritDoc} */
     @Override
-    public FacetDefinition readFacet() throws IOException {
+    public FacetDefinition readFacet() {
         if (!foundSolidEnd && parser.hasMoreCharacters()) {
             ensureSolidStarted();
 
@@ -88,15 +89,16 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
-        reader.close();
+    public void close() {
+        GeometryIOUtils.closeUnchecked(reader);
     }
 
     /** Internal method to read a single facet from the STL content.
      * @return next facet definition
-     * @throws IOException if an I/O or data format exception occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private FacetDefinition readFacetInternal() throws IOException {
+    private FacetDefinition readFacetInternal() {
         matchKeyword(StlConstants.NORMAL_KEYWORD);
         final Vector3D normal = readVector();
 
@@ -120,9 +122,10 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Ensure that an STL solid definition is in the process of being read. If not, \
                the beginning
      * of a the definition is attempted to be read from the input.
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void ensureSolidStarted() throws IOException {
+    private void ensureSolidStarted() {
         if (!foundSolidStart) {
             beginSolid();
 
@@ -132,9 +135,10 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Begin reading an STL solid definition. The "solid" keyword is read
      * along with the name of the solid.
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void beginSolid() throws IOException {
+    private void beginSolid() {
         matchKeyword(StlConstants.SOLID_START_KEYWORD);
 
         solidName = trimmedOrNull(parser.nextLine()
@@ -142,28 +146,30 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  }
 
     /** Read the next word from the content, discarding preceding whitespace.
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void nextWord() throws IOException {
+    private void nextWord() {
         parser.discardWhitespace()
             .nextAlphanumeric();
     }
 
     /** Read the next word from the content and match it against the given keyword.
      * @param keyword keyword to match against
-     * @throws IOException if an I/O error occurs or the read content does not
-     *      match the given keyword
+     * @throws IllegalStateException if the read content does not match the given \
keyword +     * @throws java.io.UncheckedIOException if an I/O error occurs or
      */
-    private void matchKeyword(final String keyword) throws IOException {
+    private void matchKeyword(final String keyword) {
         nextWord();
         parser.matchIgnoreCase(keyword);
     }
 
     /** Read a vector from the input.
      * @return the vector read from the input
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private Vector3D readVector() throws IOException {
+    private Vector3D readVector() {
         final double x = readDouble();
         final double y = readDouble();
         final double z = readDouble();
@@ -173,9 +179,10 @@ public class TextStlFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Read a double value from the input.
      * @return double value read from the input
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private double readDouble() throws IOException {
+    private double readDouble() {
         return parser
                 .discardWhitespace()
                 .next(SimpleTextParser::isDecimalPart)
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java
 index 22cf33b..6368a5e 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
-import java.io.IOException;
 import java.io.Writer;
 import java.util.List;
 
@@ -50,19 +49,19 @@ public class TextStlWriter extends AbstractTextFormatWriter {
 
     /** Write the start of an unnamed STL solid definition. This method is \
                equivalent to calling
      * {@code stlWriter.startSolid(null);}
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void startSolid() throws IOException {
+    public void startSolid() {
         startSolid(null);
     }
 
     /** Write the start of an STL solid definition with the given name.
      * @param solidName the name of the solid; may be null
-     * @throws IllegalStateException if a solid definition has already been started
      * @throws IllegalArgumentException if {@code solidName} contains new line \
                characters
-     * @throws IOException if an I/O error occurs
+     * @throws IllegalStateException if a solid definition has already been started
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void startSolid(final String solidName) throws IOException {
+    public void startSolid(final String solidName) {
         if (started) {
             throw new IllegalStateException("Cannot start solid definition: a solid \
is already being written");  }
@@ -79,9 +78,9 @@ public class TextStlWriter extends AbstractTextFormatWriter {
     /** Write the end of the current STL solid definition. This method is called \
                automatically on
      * {@link #close()} if needed.
      * @throws IllegalStateException if no solid definition has been started
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void endSolid() throws IOException {
+    public void endSolid() {
         if (!started) {
             throw new IllegalStateException("Cannot end solid definition: no solid \
has been started");  }
@@ -94,10 +93,10 @@ public class TextStlWriter extends AbstractTextFormatWriter {
     /** Write the given boundary to the output as triangles.
      * @param boundary boundary to write
      * @throws IllegalStateException if no solid has been started yet
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see PlaneConvexSubset#toTriangles()
      */
-    public void writeTriangles(final PlaneConvexSubset boundary) throws IOException \
{ +    public void writeTriangles(final PlaneConvexSubset boundary) {
         for (final Triangle3D tri : boundary.toTriangles()) {
             writeTriangles(tri.getVertices(), tri.getPlane().getNormal());
         }
@@ -106,10 +105,10 @@ public class TextStlWriter extends AbstractTextFormatWriter {
     /** Write the given facet definition to the output as triangles.
      * @param facet facet definition to write
      * @throws IllegalStateException if no solid has been started yet
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #writeTriangle(Vector3D, Vector3D, Vector3D, Vector3D)
      */
-    public void writeTriangles(final FacetDefinition facet) throws IOException {
+    public void writeTriangles(final FacetDefinition facet) {
         writeTriangles(facet.getVertices(), facet.getNormal());
     }
 
@@ -128,9 +127,9 @@ public class TextStlWriter extends AbstractTextFormatWriter {
      * @param normal facet normal; may be null
      * @throws IllegalStateException if no solid has been started yet or fewer than \
                3 vertices
      *      are given
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeTriangles(final List<Vector3D> vertices, final Vector3D normal) \
throws IOException { +    public void writeTriangles(final List<Vector3D> vertices, \
                final Vector3D normal) {
         for (final List<Vector3D> triangle : \
EuclideanUtils.convexPolygonToTriangleFan(vertices, t -> t)) {  writeTriangle(
                     triangle.get(0),
@@ -153,10 +152,9 @@ public class TextStlWriter extends AbstractTextFormatWriter {
      * @param p3 third point
      * @param normal facet normal; may be null
      * @throws IllegalStateException if no solid has been started yet
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D \
                p3, final Vector3D normal)
-            throws IOException {
+    public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D \
p3, final Vector3D normal) {  if (!started) {
             throw new IllegalStateException("Cannot write triangle: no solid has \
been started");  }
@@ -190,7 +188,7 @@ public class TextStlWriter extends AbstractTextFormatWriter {
 
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
+    public void close() {
         if (started) {
             endSolid();
         }
@@ -200,9 +198,9 @@ public class TextStlWriter extends AbstractTextFormatWriter {
 
     /** Write a triangle vertex to the output.
      * @param vertex triangle vertex
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void writeTriangleVertex(final Vector3D vertex) throws IOException {
+    private void writeTriangleVertex(final Vector3D vertex) {
         write(StlConstants.VERTEX_KEYWORD);
         write(SPACE);
         writeVector(vertex);
@@ -211,9 +209,9 @@ public class TextStlWriter extends AbstractTextFormatWriter {
 
     /** Write a vector to the output.
      * @param vec vector to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void writeVector(final Vector3D vec) throws IOException {
+    private void writeVector(final Vector3D vec) {
         write(vec.getX());
         write(SPACE);
         write(vec.getY());
@@ -223,9 +221,9 @@ public class TextStlWriter extends AbstractTextFormatWriter {
 
     /** Write the beginning or ending line of the solid definition.
      * @param keyword keyword at the start of the line
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void writeBeginOrEndLine(final String keyword) throws IOException {
+    private void writeBeginOrEndLine(final String keyword) {
         write(keyword);
         write(SPACE);
 
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/AbstractTextBoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/AbstractTextBoundaryWriteHandler3D.java
 index 58408b4..2cb3565 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/AbstractTextBoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/AbstractTextBoundaryWriteHandler3D.java
 @@ -16,9 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Iterator;
@@ -26,6 +23,7 @@ import java.util.function.DoubleFunction;
 import java.util.stream.Stream;
 
 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.core.output.GeometryOutput;
 import org.apache.commons.geometry.io.euclidean.threed.AbstractBoundaryWriteHandler3D;
  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
@@ -97,8 +95,7 @@ public abstract class AbstractTextBoundaryWriteHandler3D extends \
AbstractBoundar  
     /** {@inheritDoc} */
     @Override
-    public void write(final Stream<? extends PlaneConvexSubset> boundaries, final \
                GeometryOutput out)
-            throws IOException {
+    public void write(final Stream<? extends PlaneConvexSubset> boundaries, final \
                GeometryOutput out) {
         try (TextFacetDefinitionWriter writer = getFacetDefinitionWriter(out)) {
             final Iterator<? extends PlaneConvexSubset> it = boundaries.iterator();
             while (it.hasNext()) {
@@ -109,8 +106,7 @@ public abstract class AbstractTextBoundaryWriteHandler3D extends \
AbstractBoundar  
     /** {@inheritDoc} */
     @Override
-    public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
                GeometryOutput out)
-            throws IOException {
+    public void writeFacets(final Stream<? extends FacetDefinition> facets, final \
                GeometryOutput out) {
         try (TextFacetDefinitionWriter writer = getFacetDefinitionWriter(out)) {
             final Iterator<? extends FacetDefinition> it = facets.iterator();
             while (it.hasNext()) {
@@ -122,15 +118,11 @@ public abstract class AbstractTextBoundaryWriteHandler3D \
                extends AbstractBoundar
     /** Get a configured {@link TextFacetDefinitionWriter} for writing output.
      * @param out output stream to write to
      * @return a new, configured text format writer
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    protected TextFacetDefinitionWriter getFacetDefinitionWriter(final \
                GeometryOutput out) throws IOException {
-        final Charset charset = out.getCharset() != null ?
-                out.getCharset() :
-                defaultCharset;
-
-        final TextFacetDefinitionWriter facetWriter = new TextFacetDefinitionWriter(
-                new BufferedWriter(new OutputStreamWriter(out.getOutputStream(), \
charset))); +    protected TextFacetDefinitionWriter getFacetDefinitionWriter(final \
GeometryOutput out) { +        final TextFacetDefinitionWriter facetWriter =
+                new \
TextFacetDefinitionWriter(GeometryIOUtils.createBufferedWriter(out, defaultCharset)); \
  facetWriter.setLineSeparator(lineSeparator);
         facetWriter.setDoubleFormat(doubleFormat);
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/CsvBoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/CsvBoundaryWriteHandler3D.java
 index ec6ccbf..8a537ab 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/CsvBoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/CsvBoundaryWriteHandler3D.java
 @@ -16,8 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.IOException;
-
 import org.apache.commons.geometry.io.core.GeometryFormat;
 import org.apache.commons.geometry.io.core.output.GeometryOutput;
 import org.apache.commons.geometry.io.euclidean.threed.GeometryFormat3D;
@@ -35,7 +33,7 @@ public class CsvBoundaryWriteHandler3D extends \
AbstractTextBoundaryWriteHandler3  
     /** {@inheritDoc} */
     @Override
-    protected TextFacetDefinitionWriter getFacetDefinitionWriter(final \
GeometryOutput out) throws IOException { +    protected TextFacetDefinitionWriter \
                getFacetDefinitionWriter(final GeometryOutput out) {
         final TextFacetDefinitionWriter facetWriter = \
super.getFacetDefinitionWriter(out);  
         facetWriter.setVertexComponentSeparator(TextFacetDefinitionWriter.CSV_SEPARATOR);
                
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3D.java
 index c04d886..9819d3f 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3D.java
 @@ -16,15 +16,12 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 
 import org.apache.commons.geometry.io.core.GeometryFormat;
 import org.apache.commons.geometry.io.core.input.GeometryInput;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.euclidean.threed.AbstractBoundaryReadHandler3D;
  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
 import org.apache.commons.geometry.io.euclidean.threed.GeometryFormat3D;
@@ -62,15 +59,7 @@ public class TextBoundaryReadHandler3D extends \
AbstractBoundaryReadHandler3D {  
     /** {@inheritDoc} */
     @Override
-    public FacetDefinitionReader facetDefinitionReader(final GeometryInput in) \
                throws IOException {
-        return new TextFacetDefinitionReader(createReader(in));
-    }
-
-    private Reader createReader(final GeometryInput in) throws IOException {
-        final Charset charset = in.getCharset() != null ?
-                in.getCharset() :
-                defaultCharset;
-
-        return new BufferedReader(new InputStreamReader(in.getInputStream(), \
charset)); +    public FacetDefinitionReader facetDefinitionReader(final \
GeometryInput in) { +        return new \
TextFacetDefinitionReader(GeometryIOUtils.createBufferedReader(in, defaultCharset));  \
}  }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3D.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3D.java
 index 544f118..2c222de 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3D.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3D.java
 @@ -16,8 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.IOException;
-
 import org.apache.commons.geometry.io.core.GeometryFormat;
 import org.apache.commons.geometry.io.core.output.GeometryOutput;
 import org.apache.commons.geometry.io.euclidean.threed.GeometryFormat3D;
@@ -99,7 +97,7 @@ public class TextBoundaryWriteHandler3D extends \
AbstractTextBoundaryWriteHandler  
     /** {@inheritDoc} */
     @Override
-    protected TextFacetDefinitionWriter getFacetDefinitionWriter(final \
GeometryOutput out) throws IOException { +    protected TextFacetDefinitionWriter \
                getFacetDefinitionWriter(final GeometryOutput out) {
         final TextFacetDefinitionWriter facetWriter = \
super.getFacetDefinitionWriter(out);  
         facetWriter.setVertexComponentSeparator(vertexComponentSeparator);
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReader.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReader.java
 index d2faf30..d03d2fe 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReader.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReader.java
 @@ -16,13 +16,13 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
 import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
@@ -136,7 +136,7 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** {@inheritDoc} */
     @Override
-    public FacetDefinition readFacet() throws IOException {
+    public FacetDefinition readFacet() {
         discardNonDataLines();
         if (parser.hasMoreCharacters()) {
             try {
@@ -152,16 +152,17 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** {@inheritDoc} */
     @Override
-    public void close() throws IOException {
-        reader.close();
+    public void close() {
+        GeometryIOUtils.closeUnchecked(reader);
     }
 
     /** Internal method to read a facet definition starting from the current parser
      * position. Empty lines (including lines containing only comments) are \
                discarded.
      * @return facet definition or null if the end of input is reached
-     * @throws IOException if an I/O or parsing error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private FacetDefinition readFacetInternal() throws IOException {
+    private FacetDefinition readFacetInternal() {
         final Vector3D p1 = readVector();
         discardNonData();
         final Vector3D p2 = readVector();
@@ -190,9 +191,10 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Read a vector starting from the current parser position.
      * @return vector read from the parser
-     * @throws IOException if an I/O or parsing error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private Vector3D readVector() throws IOException {
+    private Vector3D readVector() {
         final double x = readDouble();
         discardNonData();
         final double y = readDouble();
@@ -204,9 +206,10 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Read a double starting from the current parser position.
      * @return double value read from the parser
-     * @throws IOException if an I/O or parsing error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private double readDouble() throws IOException {
+    private double readDouble() {
         return parser
                 .next(TextFacetDefinitionReader::isDataTokenPart)
                 .getCurrentTokenAsDouble();
@@ -214,9 +217,10 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Discard lines that do not contain any data. This includes empty lines
      * and lines that only contain comments.
-     * @throws IOException if an I/O or parsing error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void discardNonDataLines() throws IOException {
+    private void discardNonDataLines() {
         parser.discardLineWhitespace();
         while (parser.hasMoreCharacters() &&
                 (!parser.hasMoreCharactersOnLine() ||
@@ -230,9 +234,10 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Discard a sequence of non-data characters on the current line starting
      * from the current parser position.
-     * @throws IOException if an I/O or parsing error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void discardNonData() throws IOException {
+    private void discardNonData() {
         parser.discard(c ->
             !SimpleTextParser.isNewLinePart(c) &&
             !isDataTokenPart(c) &&
@@ -247,9 +252,10 @@ public class TextFacetDefinitionReader implements \
FacetDefinitionReader {  
     /** Return true if the parser is positioned at the start of the comment token.
      * @return true if the parser is positioned at the start of the comment token.
-     * @throws IOException if an I/O or parsing error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private boolean foundComment() throws IOException {
+    private boolean foundComment() {
         return hasCommentToken &&
                 commentToken.equals(parser.peek(commentToken.length()));
     }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriter.java \
b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriter.java
 index ed94c1d..c23c47b 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriter.java
                
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriter.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.IOException;
 import java.io.Writer;
 import java.util.Iterator;
 import java.util.List;
@@ -192,9 +191,9 @@ public class TextFacetDefinitionWriter extends \
AbstractTextFormatWriter {  /** Write a comment to the output.
      * @param comment comment string to write
      * @throws IllegalStateException if the configured {@link #getCommentToken() \
                comment token} is null
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeComment(final String comment) throws IOException {
+    public void writeComment(final String comment) {
         if (commentToken == null) {
             throw new IllegalStateException("Cannot write comment: no comment token \
configured");  }
@@ -208,9 +207,9 @@ public class TextFacetDefinitionWriter extends \
AbstractTextFormatWriter {  }
 
     /** Write a blank line to the output.
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void writeBlankLine() throws IOException {
+    public void writeBlankLine() {
         writeNewLine();
     }
 
@@ -222,9 +221,9 @@ public class TextFacetDefinitionWriter extends \
                AbstractTextFormatWriter {
      * @throws IllegalArgumentException if any boundary has infinite size or a
      *      {@link #getFacetVertexCount() facet vertex count} has been configured \
                and a boundary
      *      cannot be represented using the required number of vertices
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void write(final BoundarySource3D src) throws IOException {
+    public void write(final BoundarySource3D src) {
         try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
             final Iterator<PlaneConvexSubset> it = stream.iterator();
             while (it.hasNext()) {
@@ -241,9 +240,9 @@ public class TextFacetDefinitionWriter extends \
                AbstractTextFormatWriter {
      * @throws IllegalArgumentException if the argument has infinite size or a
      *      {@link #getFacetVertexCount() facet vertex count} has been configured \
                and the number of required
      *      vertices does not match the number present in the argument
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void write(final PlaneConvexSubset convexSubset) throws IOException {
+    public void write(final PlaneConvexSubset convexSubset) {
         if (convexSubset.isInfinite()) {
             throw new IllegalArgumentException("Cannot write infinite convex \
subset");  }
@@ -265,9 +264,9 @@ public class TextFacetDefinitionWriter extends \
                AbstractTextFormatWriter {
      * @throws IllegalArgumentException if a {@link #getFacetVertexCount() facet \
                vertex count}
      *      has been configured and the number of required vertices does not match \
                the number
      *      present in the argument
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public void write(final FacetDefinition facet) throws IOException {
+    public void write(final FacetDefinition facet) {
         write(facet.getVertices());
     }
 
@@ -279,12 +278,12 @@ public class TextFacetDefinitionWriter extends \
                AbstractTextFormatWriter {
      * @throws IllegalArgumentException if the vertex list contains less than 3 \
                vertices or a
      *      {@link #getFacetVertexCount() facet vertex count} has been configured \
                and the number of required
      *      vertices does not match the number given
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      * @see #getVertexComponentSeparator()
      * @see #getVertexSeparator()
      * @see #getFacetVertexCount()
      */
-    public void write(final List<Vector3D> vertices) throws IOException {
+    public void write(final List<Vector3D> vertices) {
         final int size = vertices.size();
         if (size < 3) {
             throw new IllegalArgumentException("At least 3 vertices are required per \
facet; found " + size); @@ -306,9 +305,9 @@ public class TextFacetDefinitionWriter \
extends AbstractTextFormatWriter {  
     /** Write a single vertex to the output.
      * @param vertex vertex to write
-     * @throws IOException if an I/O error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    private void write(final Vector3D vertex) throws IOException {
+    private void write(final Vector3D vertex) {
         write(vertex.getX());
         write(vertexComponentSeparator);
         write(vertex.getY());
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/DocumentationExamplesTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/DocumentationExamplesTest.java
 index 024b653..1ec5625 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/DocumentationExamplesTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/DocumentationExamplesTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean;
 
-import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.stream.Stream;
@@ -47,7 +46,7 @@ class DocumentationExamplesTest {
     Path tempDir;
 
     @Test
-    void testIndexPageExample() throws IOException {
+    void testIndexPageExample() {
         // construct a precision instance to handle floating-point comparisons
         final Precision.DoubleEquivalence precision = \
Precision.doubleEquivalenceOfEpsilon(1e-6);  
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/EuclideanIOTestUtils.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/EuclideanIOTestUtils.java
 index 40ea358..87e12c7 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/EuclideanIOTestUtils.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/EuclideanIOTestUtils.java
 @@ -128,9 +128,10 @@ public final class EuclideanIOTestUtils {
     /** Read all facets available from the given facet reader.
      * @param reader instance to read facets from
      * @return list containing all facets available from the given facet reader
-     * @throws IOException if an I/O or data format error occurs
+     * @throws IllegalStateException if a data format error occurs
+     * @throws java.io.UncheckedIOException if an I/O error occurs
      */
-    public static List<FacetDefinition> readAll(final FacetDefinitionReader reader) \
throws IOException { +    public static List<FacetDefinition> readAll(final \
FacetDefinitionReader reader) {  final List<FacetDefinition> facets = new \
ArrayList<>();  
         FacetDefinition f;
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3DTest.java
 index 88a6c66..58f4591 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/AbstractBoundaryReadHandler3DTest.java
 @@ -17,8 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed;
 
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
@@ -56,7 +54,7 @@ class AbstractBoundaryReadHandler3DTest {
             Vector3D.ZERO, Vector3D.of(0, 1, 0), Vector3D.of(-1, 1, 0), \
Vector3D.of(-1, 0, 0)));  
     @Test
-    void testRead() throws IOException {
+    void testRead() {
         // arrange
         final List<FacetDefinition> facets = Arrays.asList(FACET_1, FACET_2);
         final TestReadHandler3D handler = new TestReadHandler3D(facets);
@@ -74,7 +72,7 @@ class AbstractBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testReadTriangleMesh() throws IOException {
+    void testReadTriangleMesh() {
         // arrange
         final List<FacetDefinition> facets = Arrays.asList(FACET_1, FACET_2);
         final TestReadHandler3D handler = new TestReadHandler3D(facets);
@@ -93,7 +91,7 @@ class AbstractBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testBoundaries() throws IOException {
+    void testBoundaries() {
         // arrange
         final List<FacetDefinition> facets = Arrays.asList(FACET_1, FACET_2);
         final TestReadHandler3D handler = new TestReadHandler3D(facets);
@@ -117,7 +115,7 @@ class AbstractBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacets() throws IOException {
+    void testFacets() {
         // arrange
         final List<FacetDefinition> facets = Arrays.asList(FACET_1, FACET_2);
         final TestReadHandler3D handler = new TestReadHandler3D(facets);
@@ -166,7 +164,7 @@ class AbstractBoundaryReadHandler3DTest {
         final FacetDefinitionReaderIterator it = new \
FacetDefinitionReaderIterator(reader);  
         // act/assert
-        GeometryTestUtils.assertThrowsWithMessage(it::next, \
UncheckedIOException.class, "IOException: Read failure"); +        \
GeometryTestUtils.assertThrowsWithMessage(it::next, IllegalStateException.class, \
"Read failure");  }
 
     private static final class TestReadHandler3D extends \
AbstractBoundaryReadHandler3D { @@ -187,7 +185,7 @@ class \
AbstractBoundaryReadHandler3DTest {  
         /** {@inheritDoc} */
         @Override
-        public FacetDefinitionReader facetDefinitionReader(final GeometryInput in) \
throws IOException { +        public FacetDefinitionReader \
facetDefinitionReader(final GeometryInput in) {  this.inArg = in;
 
             return new StubFacetDefinitionReader(facets);
@@ -206,9 +204,9 @@ class AbstractBoundaryReadHandler3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public FacetDefinition readFacet() throws IOException {
+        public FacetDefinition readFacet() {
             if (fail) {
-                throw new IOException("Read failure");
+                throw new IllegalStateException("Read failure");
             }
 
             return iterator.hasNext() ?
@@ -218,7 +216,7 @@ class AbstractBoundaryReadHandler3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public void close() throws IOException {
+        public void close() {
             // do nothing
         }
     }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3DTest.java
 index c04769c..b54da73 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/BoundaryIOManager3DTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed;
 
-import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
@@ -53,12 +52,12 @@ class BoundaryIOManager3DTest {
     private static final FacetDefinitionReader FACET_DEF_READER = new \
FacetDefinitionReader() {  
         @Override
-        public FacetDefinition readFacet() throws IOException {
+        public FacetDefinition readFacet() {
             throw new UnsupportedOperationException();
         }
 
         @Override
-        public void close() throws IOException {
+        public void close() {
             // do nothing
         }
     };
@@ -92,7 +91,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_formatGiven() throws IOException {
+    void testFacetDefinitionReader_formatGiven() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -108,7 +107,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_nullFormat() throws IOException {
+    void testFacetDefinitionReader_nullFormat() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -124,13 +123,13 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_unknownHandler() throws IOException {
+    void testFacetDefinitionReader_unknownHandler() {
         // act/assert
         checkUnknownReadHandler(manager::facetDefinitionReader);
     }
 
     @Test
-    void testFacets_formatGiven() throws IOException {
+    void testFacets_formatGiven() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -146,7 +145,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testFacets_nullFormat() throws IOException {
+    void testFacets_nullFormat() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -162,13 +161,13 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testFacets_unknownHandler() throws IOException {
+    void testFacets_unknownHandler() {
         // act/assert
         checkUnknownReadHandler(manager::facets);
     }
 
     @Test
-    void testTriangles_formatGiven() throws IOException {
+    void testTriangles_formatGiven() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -185,7 +184,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testTriangles_nullFormat() throws IOException {
+    void testTriangles_nullFormat() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -202,13 +201,13 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testTriangles_unknownHandler() throws IOException {
+    void testTriangles_unknownHandler() {
         // act/assert
         checkUnknownReadHandler((in, fmt) -> manager.triangles(in, fmt, \
TEST_PRECISION));  }
 
     @Test
-    void testReadTriangleMesh_formatGiven() throws IOException {
+    void testReadTriangleMesh_formatGiven() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -225,7 +224,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testReadTriangleMesh_nullFormat() throws IOException {
+    void testReadTriangleMesh_nullFormat() {
         // arrange
         final StubReadHandler3D readHandler = new StubReadHandler3D();
         manager.registerReadHandler(readHandler);
@@ -242,13 +241,13 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testReadTriangleMesh_unknownHandler() throws IOException {
+    void testReadTriangleMesh_unknownHandler() {
         // act/assert
         checkUnknownReadHandler((in, fmt) -> manager.readTriangleMesh(in, fmt, \
TEST_PRECISION));  }
 
     @Test
-    void testWrite_stream_formatGiven() throws IOException {
+    void testWrite_stream_formatGiven() {
         // arrange
         final StubWriteHandler3D writeHandler = new StubWriteHandler3D();
         manager.registerWriteHandler(writeHandler);
@@ -264,7 +263,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testWrite_stream_nullFormat() throws IOException {
+    void testWrite_stream_nullFormat() {
         // arrange
         final StubWriteHandler3D writeHandler = new StubWriteHandler3D();
         manager.registerWriteHandler(writeHandler);
@@ -280,13 +279,13 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testWrite_stream_unknownHandler() throws IOException {
+    void testWrite_stream_unknownHandler() {
         // act/assert
         checkUnknownWriteHandler((out, fmt) -> manager.write(Stream.of(TRI), out, \
fmt));  }
 
     @Test
-    void testWriteFacets_stream_formatGiven() throws IOException {
+    void testWriteFacets_stream_formatGiven() {
         // arrange
         final StubWriteHandler3D writeHandler = new StubWriteHandler3D();
         manager.registerWriteHandler(writeHandler);
@@ -302,7 +301,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testWriteFacets_stream_nullFormat() throws IOException {
+    void testWriteFacets_stream_nullFormat() {
         // arrange
         final StubWriteHandler3D writeHandler = new StubWriteHandler3D();
         manager.registerWriteHandler(writeHandler);
@@ -318,13 +317,13 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testWriteFacets_stream_unknownHandler() throws IOException {
+    void testWriteFacets_stream_unknownHandler() {
         // act/assert
         checkUnknownWriteHandler((out, fmt) -> manager.writeFacets(Stream.of(FACET), \
out, fmt));  }
 
     @Test
-    void testWriteFacets_collection_formatGiven() throws IOException {
+    void testWriteFacets_collection_formatGiven() {
         // arrange
         final StubWriteHandler3D writeHandler = new StubWriteHandler3D();
         manager.registerWriteHandler(writeHandler);
@@ -340,7 +339,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testWriteFacets_collection_nullFormat() throws IOException {
+    void testWriteFacets_collection_nullFormat() {
         // arrange
         final StubWriteHandler3D writeHandler = new StubWriteHandler3D();
         manager.registerWriteHandler(writeHandler);
@@ -356,7 +355,7 @@ class BoundaryIOManager3DTest {
     }
 
     @Test
-    void testWriteFacets_collection_unknownHandler() throws IOException {
+    void testWriteFacets_collection_unknownHandler() {
         // act/assert
         checkUnknownWriteHandler((out, fmt) -> \
manager.writeFacets(Collections.singletonList(FACET), out, fmt));  }
@@ -418,15 +417,14 @@ class BoundaryIOManager3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public BoundarySource3D read(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-                throws IOException {
+        public BoundarySource3D read(final GeometryInput in, final \
Precision.DoubleEquivalence precision) {  throw new UnsupportedOperationException();
         }
 
         /** {@inheritDoc} */
         @Override
         public Stream<PlaneConvexSubset> boundaries(final GeometryInput in,
-                final Precision.DoubleEquivalence precision) throws IOException {
+                final Precision.DoubleEquivalence precision) {
             this.inArg = in;
             this.precisionArg = precision;
 
@@ -435,7 +433,7 @@ class BoundaryIOManager3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public FacetDefinitionReader facetDefinitionReader(final GeometryInput in) \
throws IOException { +        public FacetDefinitionReader \
facetDefinitionReader(final GeometryInput in) {  this.inArg = in;
 
             return FACET_DEF_READER;
@@ -443,7 +441,7 @@ class BoundaryIOManager3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public Stream<FacetDefinition> facets(final GeometryInput in) throws \
IOException { +        public Stream<FacetDefinition> facets(final GeometryInput in) \
{  this.inArg = in;
 
             return Stream.of(FACET);
@@ -451,8 +449,7 @@ class BoundaryIOManager3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public TriangleMesh readTriangleMesh(final GeometryInput in, final \
                Precision.DoubleEquivalence precision)
-                throws IOException {
+        public TriangleMesh readTriangleMesh(final GeometryInput in, final \
Precision.DoubleEquivalence precision) {  this.inArg = in;
             this.precisionArg = precision;
 
@@ -476,30 +473,27 @@ class BoundaryIOManager3DTest {
 
         /** {@inheritDoc} */
         @Override
-        public void write(final Stream<? extends PlaneConvexSubset> boundaries, \
                final GeometryOutput out)
-                throws IOException {
+        public void write(final Stream<? extends PlaneConvexSubset> boundaries, \
final GeometryOutput out) {  this.boundariesArg = \
boundaries.collect(Collectors.toList());  this.outArg = out;
         }
 
         /** {@inheritDoc} */
         @Override
-        public void write(final BoundarySource3D src, final GeometryOutput out) \
throws IOException { +        public void write(final BoundarySource3D src, final \
GeometryOutput out) {  throw new UnsupportedOperationException();
         }
 
         /** {@inheritDoc} */
         @Override
-        public void writeFacets(final Stream<? extends FacetDefinition> facets, \
                final GeometryOutput out)
-                throws IOException {
+        public void writeFacets(final Stream<? extends FacetDefinition> facets, \
final GeometryOutput out) {  this.facetsArg = facets.collect(Collectors.toList());
             this.outArg = out;
         }
 
         /** {@inheritDoc} */
         @Override
-        public void writeFacets(final Collection<? extends FacetDefinition> facets, \
                final GeometryOutput out)
-                throws IOException {
+        public void writeFacets(final Collection<? extends FacetDefinition> facets, \
final GeometryOutput out) {  this.facetsArg = facets;
             this.outArg = out;
         }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/IO3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/IO3DTest.java
 index dc9b961..28e30da 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/IO3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/IO3DTest.java
 @@ -69,7 +69,7 @@ class IO3DTest {
     public Path tempDir;
 
     @Test
-    void testStreamExample() throws IOException {
+    void testStreamExample() {
         final Path origFile = tempDir.resolve("orig.obj");
         final Path scaledFile = tempDir.resolve("scaled.csv");
 
@@ -296,21 +296,24 @@ class IO3DTest {
         final Path tmp = Files.createTempFile("tmp", "." + \
fmt.getDefaultFileExtension());  
         final BoundarySource3D orig;
-        try (CloseCountInputStream in = new CloseCountInputStream(new \
BufferedInputStream(Files.newInputStream(path)))) { +        try \
(CloseCountInputStream in = +                new CloseCountInputStream(new \
BufferedInputStream(Files.newInputStream(path)))) {  orig = readFn.read(fmt, new \
StreamGeometryInput(in));  
             Assertions.assertEquals(1, in.getCloseCount());
         }
         assertRegion(expected, orig);
 
-        try (CloseCountOutputStream out = new CloseCountOutputStream(new \
BufferedOutputStream(Files.newOutputStream(tmp)))) { +        try \
(CloseCountOutputStream out = +                new CloseCountOutputStream(new \
BufferedOutputStream(Files.newOutputStream(tmp)))) {  writeFn.write(orig, fmt, new \
StreamGeometryOutput(out));  
             Assertions.assertEquals(1, out.getCloseCount());
         }
 
         final BoundarySource3D result;
-        try (CloseCountInputStream in = new CloseCountInputStream(new \
BufferedInputStream(Files.newInputStream(tmp)))) { +        try \
(CloseCountInputStream in = +                new CloseCountInputStream(new \
BufferedInputStream(Files.newInputStream(tmp)))) {  result = readFn.read(fmt, new \
StreamGeometryInput(in));  }
         assertRegion(expected, result);
@@ -348,7 +351,7 @@ class IO3DTest {
         return inputs;
     }
 
-    private static BoundaryList3D readerToBoundaryList(final FacetDefinitionReader \
reader) throws IOException { +    private static BoundaryList3D \
readerToBoundaryList(final FacetDefinitionReader reader) {  try \
(FacetDefinitionReader toClose = reader) {  final List<PlaneConvexSubset> list = new \
ArrayList<>();  FacetDefinition f;
@@ -360,7 +363,7 @@ class IO3DTest {
         }
     }
 
-    private static BoundaryList3D facetsToBoundaryList(final Stream<FacetDefinition> \
stream) throws IOException { +    private static BoundaryList3D \
facetsToBoundaryList(final Stream<FacetDefinition> stream) {  try \
(Stream<FacetDefinition> facetStream = stream) {  final List<PlaneConvexSubset> list \
                = facetStream
                     .map(f -> FacetDefinitions.toPolygon(f, MODEL_PRECISION))
@@ -370,8 +373,7 @@ class IO3DTest {
         }
     }
 
-    private static <T extends PlaneConvexSubset> BoundaryList3D \
                boundariesToBoundaryList(final Stream<T> stream)
-            throws IOException {
+    private static <T extends PlaneConvexSubset> BoundaryList3D \
boundariesToBoundaryList(final Stream<T> stream) {  try (Stream<T> boundaryStream = \
                stream) {
             final List<PlaneConvexSubset> list = \
boundaryStream.collect(Collectors.toList());  
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3DTest.java
 index 73faf1e..28908c0 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryReadHandler3DTest.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -54,7 +53,7 @@ class ObjBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader() throws IOException {
+    void testFacetDefinitionReader() {
         // arrange
         final InputStream in = input(
                 "v 0 0 0\n" +
@@ -74,7 +73,7 @@ class ObjBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_usesInputCharset() throws IOException {
+    void testFacetDefinitionReader_usesInputCharset() {
         // arrange
         final InputStream in = input(
                 "v 0 0 0\n" +
@@ -95,7 +94,7 @@ class ObjBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_setDefaultCharset() throws IOException {
+    void testFacetDefinitionReader_setDefaultCharset() {
         // arrange
         handler.setDefaultCharset(StandardCharsets.UTF_16);
         final InputStream in = input(
@@ -116,7 +115,7 @@ class ObjBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_close() throws IOException {
+    void testFacetDefinitionReader_close() {
         // arrange
         final CloseCountInputStream in = input("", StandardCharsets.UTF_8);
 
@@ -129,7 +128,7 @@ class ObjBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testReadTriangleMesh() throws IOException {
+    void testReadTriangleMesh() {
         // arrange
         final CloseCountInputStream in = input(
                 "v 0 0 0\n" +
@@ -152,7 +151,7 @@ class ObjBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testReadTriangleMesh_nonDefaultCharset() throws IOException {
+    void testReadTriangleMesh_nonDefaultCharset() {
         // arrange
         handler.setDefaultCharset(StandardCharsets.UTF_16);
         final CloseCountInputStream in = input(
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3DTest.java
 index b3b474f..6a96e4e 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjBoundaryWriteHandler3DTest.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
@@ -64,7 +63,7 @@ class ObjBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets() throws IOException {
+    void testWriteFacets() {
         // arrange
         final DecimalFormat fmt =
                 new DecimalFormat("0.0#####", \
DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @@ -85,7 +84,7 @@ class \
ObjBoundaryWriteHandler3DTest {  }
 
     @Test
-    void testWriteFacets_usesOutputCharset() throws IOException {
+    void testWriteFacets_usesOutputCharset() {
         // arrange
         final DecimalFormat fmt =
                 new DecimalFormat("0.0#####", \
DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @@ -106,7 +105,7 @@ class \
ObjBoundaryWriteHandler3DTest {  }
 
     @Test
-    void testWriteFacets_customConfig() throws IOException {
+    void testWriteFacets_customConfig() {
         // arrange
         // arrange
         final DecimalFormat fmt =
@@ -134,7 +133,7 @@ class ObjBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite() throws IOException {
+    void testWrite() {
         // arrange
         final BoundarySource3D src = BoundarySource3D.of(FACETS.stream()
                 .map(f -> FacetDefinitions.toPolygon(f, TEST_PRECISION))
@@ -155,7 +154,7 @@ class ObjBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite_customConfig() throws IOException {
+    void testWrite_customConfig() {
         // arrange
         final BoundarySource3D src = BoundarySource3D.of(FACETS.stream()
                 .map(f -> FacetDefinitions.toPolygon(f, TEST_PRECISION))
@@ -187,7 +186,7 @@ class ObjBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite_mesh() throws IOException {
+    void testWrite_mesh() {
         // arrange
         final SimpleTriangleMesh.Builder builder = \
                SimpleTriangleMesh.builder(TEST_PRECISION);
         builder.addFaceAndVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), \
                Vector3D.of(0, 1, 0));
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReaderTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReaderTest.java
 index 6f3ee59..9eab16d 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReaderTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjFacetDefinitionReaderTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.util.Arrays;
 import java.util.List;
@@ -45,7 +44,7 @@ class ObjFacetDefinitionReaderTest {
     }
 
     @Test
-    void testClose() throws IOException {
+    void testClose() {
         // arrange
         final CloseCountReader closeReader = new CloseCountReader(new \
StringReader(""));  
@@ -58,7 +57,7 @@ class ObjFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_withNormal() throws IOException {
+    void testReadFacet_withNormal() {
         // arrange
         final ObjFacetDefinitionReader reader = reader(
                 "o test\n\n" +
@@ -82,7 +81,7 @@ class ObjFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_withoutNormal() throws IOException {
+    void testReadFacet_withoutNormal() {
         // arrange
         final ObjFacetDefinitionReader reader = reader(
                 "o test\n\n" +
@@ -103,7 +102,7 @@ class ObjFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_failOnNonPolygon() throws IOException {
+    void testReadFacet_failOnNonPolygon() {
         // arrange
         final ObjFacetDefinitionReader reader = reader(
                 "o test\n\n" +
@@ -120,7 +119,7 @@ class ObjFacetDefinitionReaderTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(
                 () -> EuclideanIOTestUtils.readAll(reader),
-                IOException.class, Pattern.compile("^Parsing failed.*"));
+                IllegalStateException.class, Pattern.compile("^Parsing failed.*"));
     }
 
     private static ObjFacetDefinitionReader reader(final String str) {
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReaderTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReaderTest.java
 index 2fe898b..fe2ba1f 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReaderTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjTriangleMeshReaderTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.util.Arrays;
 import java.util.regex.Pattern;
@@ -47,7 +46,7 @@ class ObjTriangleMeshReaderTest {
     }
 
     @Test
-    void testClose() throws IOException {
+    void testClose() {
         // arrange
         final CloseCountReader closeReader = new CloseCountReader(new \
StringReader(""));  
@@ -60,7 +59,7 @@ class ObjTriangleMeshReaderTest {
     }
 
     @Test
-    void testReadTriangleMesh_withNormal() throws IOException {
+    void testReadTriangleMesh_withNormal() {
         // arrange
         final ObjTriangleMeshReader reader = reader(
                 "o test\n\n" +
@@ -99,7 +98,7 @@ class ObjTriangleMeshReaderTest {
     }
 
     @Test
-    void testReadTriangleMesh_withoutNormal() throws IOException {
+    void testReadTriangleMesh_withoutNormal() {
         // arrange
         final ObjTriangleMeshReader reader = reader(
                 "o test\n\n" +
@@ -128,7 +127,7 @@ class ObjTriangleMeshReaderTest {
     }
 
     @Test
-    void testReadTriangleMesh_failOnNonPolygon() throws IOException {
+    void testReadTriangleMesh_failOnNonPolygon() {
         // arrange
         final ObjTriangleMeshReader reader = reader(
                 "o test\n\n" +
@@ -145,7 +144,7 @@ class ObjTriangleMeshReaderTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(
                 () -> reader.readTriangleMesh(),
-                IOException.class, Pattern.compile("^Parsing failed.*"));
+                IllegalStateException.class, Pattern.compile("^Parsing failed.*"));
     }
 
     private static ObjTriangleMeshReader reader(final String str) {
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriterTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriterTest.java
 index 61d58cc..117ae45 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriterTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/ObjWriterTest.java
 @@ -16,9 +16,7 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.StringWriter;
-import java.io.UncheckedIOException;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.util.Arrays;
@@ -43,7 +41,7 @@ class ObjWriterTest {
             Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
 
     @Test
-    void testPropertyDefaults() throws IOException {
+    void testPropertyDefaults() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -57,7 +55,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testClose_calledMultipleTimes() throws IOException {
+    void testClose_calledMultipleTimes() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -70,7 +68,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testSetLineSeparator() throws IOException {
+    void testSetLineSeparator() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -91,7 +89,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testSetDecimalFormat() throws IOException {
+    void testSetDecimalFormat() {
         // arrange
         final StringWriter writer = new StringWriter();
         final DecimalFormat fmt =
@@ -109,7 +107,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteComment() throws IOException {
+    void testWriteComment() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -128,7 +126,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteObjectName() throws IOException {
+    void testWriteObjectName() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -142,7 +140,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteGroupName() throws IOException {
+    void testWriteGroupName() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -156,7 +154,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteVertex() throws IOException {
+    void testWriteVertex() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -187,7 +185,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteNormal() throws IOException {
+    void testWriteNormal() {
         // arrange
         final StringWriter writer = new StringWriter();
         final DecimalFormat fmt =
@@ -216,7 +214,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteFace() throws IOException {
+    void testWriteFace() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -242,7 +240,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteFace_withNormals() throws IOException {
+    void testWriteFace_withNormals() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -273,7 +271,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteFace_invalidVertexNumber() throws IOException {
+    void testWriteFace_invalidVertexNumber() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -286,7 +284,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteFace_vertexIndexOutOfBounds() throws IOException {
+    void testWriteFace_vertexIndexOutOfBounds() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -311,7 +309,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteFace_normalIndexOutOfBounds() throws IOException {
+    void testWriteFace_normalIndexOutOfBounds() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -342,7 +340,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteFace_invalidVertexAndNormalCountMismatch() throws IOException {
+    void testWriteFace_invalidVertexAndNormalCountMismatch() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -355,7 +353,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteMesh() throws IOException {
+    void testWriteMesh() {
         // arrange
         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), \
Vector3D.of(0, 1, 0)) @@ -380,7 +378,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testMeshBuffer() throws IOException {
+    void testMeshBuffer() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -413,7 +411,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testMeshBuffer_givenBatchSize() throws IOException {
+    void testMeshBuffer_givenBatchSize() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -448,7 +446,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testMeshBuffer_mixedWithDirectlyAddedFace() throws IOException {
+    void testMeshBuffer_mixedWithDirectlyAddedFace() {
         // arrange
         final StringWriter writer = new StringWriter();
 
@@ -497,7 +495,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteBoundaries_meshArgument() throws IOException {
+    void testWriteBoundaries_meshArgument() {
         // arrange
         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), \
Vector3D.of(0, 1, 0)) @@ -522,7 +520,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteBoundaries_nonMeshArgument() throws IOException {
+    void testWriteBoundaries_nonMeshArgument() {
         // arrange
         final BoundarySource3D src = BoundarySource3D.of(
                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), \
Vector3D.of(0, 1, 0), TEST_PRECISION), @@ -547,7 +545,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteBoundaries_nonMeshArgument_smallBatchSize() throws IOException {
+    void testWriteBoundaries_nonMeshArgument_smallBatchSize() {
         // arrange
         final BoundarySource3D src = BoundarySource3D.of(
                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), \
Vector3D.of(0, 1, 0), TEST_PRECISION), @@ -574,7 +572,7 @@ class ObjWriterTest {
     }
 
     @Test
-    void testWriteBoundaries_infiniteBoundary() throws IOException {
+    void testWriteBoundaries_infiniteBoundary() {
         // arrange
         final BoundarySource3D src = BoundarySource3D.of(
                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), \
Vector3D.of(0, 1, 0), TEST_PRECISION), @@ -587,8 +585,6 @@ class ObjWriterTest {
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             try (ObjWriter objWriter = new ObjWriter(writer)) {
                 objWriter.writeBoundaries(src);
-            } catch (final IOException exc) {
-                throw new UncheckedIOException(exc);
             }
         }, IllegalArgumentException.class, Pattern.compile("^OBJ input geometry \
cannot be infinite: .*"));  }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParserTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParserTest.java
 index 481bdf0..0e8afc9 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParserTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParserTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.obj;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -48,7 +47,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testNextKeyword() throws IOException {
+    void testNextKeyword() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "#comment",
@@ -81,7 +80,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testNextKeyword_polygonKeywordsOnly_valid() throws IOException {
+    void testNextKeyword_polygonKeywordsOnly_valid() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "v",
@@ -111,7 +110,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testNextKeyword_polygonKeywordsOnly_invalid() throws IOException {
+    void testNextKeyword_polygonKeywordsOnly_invalid() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "",
@@ -122,13 +121,13 @@ class PolygonObjParserTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.nextKeyword();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 1: expected keyword to be one of " \
                +
                 "[f, g, mtllib, o, s, usemtl, v, vn, vt] but was [curv2]");
     }
 
     @Test
-    void testNextKeyword_emptyContent() throws IOException {
+    void testNextKeyword_emptyContent() {
         // arrange
         final PolygonObjParser p = parser("");
 
@@ -137,7 +136,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testNextKeyword_unexpectedContent() throws IOException {
+    void testNextKeyword_unexpectedContent() {
         // arrange
         final PolygonObjParser p = parser(lines(
                     " f",
@@ -147,17 +146,17 @@ class PolygonObjParserTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.nextKeyword();
-        }, IOException.class, "Parsing failed at line 1, column 2: " +
+        }, IllegalStateException.class, "Parsing failed at line 1, column 2: " +
             "non-blank lines must begin with an OBJ keyword or comment character");
 
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.nextKeyword();
-        }, IOException.class, "Parsing failed at line 2, column 1: " +
+        }, IllegalStateException.class, "Parsing failed at line 2, column 1: " +
             "expected OBJ keyword but found empty token followed by [-]");
     }
 
     @Test
-    void testReadDataLine() throws IOException {
+    void testReadDataLine() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "  line\t",
@@ -177,7 +176,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testDiscardDataLine() throws IOException {
+    void testDiscardDataLine() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "  line\t",
@@ -208,7 +207,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testReadVector() throws IOException {
+    void testReadVector() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "1.01 3e-02 123.999 extra"
@@ -219,7 +218,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testReadVector_parseFailures() throws IOException {
+    void testReadVector_parseFailures() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "0.1 0.2 a",
@@ -230,17 +229,17 @@ class PolygonObjParserTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readVector();
-        }, IOException.class, "Parsing failed at line 1, column 9: expected double \
but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 9: expected double but found [a]");  
         p.readDataLine();
 
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readVector();
-        }, IOException.class, "Parsing failed at line 2, column 2: expected double \
but found end of line"); +        }, IllegalStateException.class, "Parsing failed at \
line 2, column 2: expected double but found end of line");  }
 
     @Test
-    void testReadDoubles() throws IOException {
+    void testReadDoubles() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "0.1 0.2 3e2 4e2 500.01",
@@ -269,7 +268,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testReadDoubles_parseFailures() throws IOException {
+    void testReadDoubles_parseFailures() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "0.1 0.2 a",
@@ -279,17 +278,17 @@ class PolygonObjParserTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readDoubles();
-        }, IOException.class, "Parsing failed at line 1, column 9: expected double \
but found [a]"); +        }, IllegalStateException.class, "Parsing failed at line 1, \
column 9: expected double but found [a]");  
         p.readDataLine();
 
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readDoubles();
-        }, IOException.class, "Parsing failed at line 2, column 1: expected double \
but found [b]"); +        }, IllegalStateException.class, "Parsing failed at line 2, \
column 1: expected double but found [b]");  }
 
     @Test
-    void testReadFace() throws IOException {
+    void testReadFace() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "# test content",
@@ -394,7 +393,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testReadFace_notEnoughVertices() throws IOException {
+    void testReadFace_notEnoughVertices() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "# test content",
@@ -408,12 +407,12 @@ class PolygonObjParserTest {
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 5, column 6: " +
+        }, IllegalStateException.class, "Parsing failed at line 5, column 6: " +
             "face must contain at least 3 vertices but found only 2");
     }
 
     @Test
-    void testReadFace_invalidVertexIndex() throws IOException {
+    void testReadFace_invalidVertexIndex() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "# test content",
@@ -430,30 +429,30 @@ class PolygonObjParserTest {
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 2, column 3: " +
+        }, IllegalStateException.class, "Parsing failed at line 2, column 3: " +
             "vertex index cannot be used because no values of that type have been \
defined");  
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 6, column 7: " +
+        }, IllegalStateException.class, "Parsing failed at line 6, column 7: " +
             "vertex index must evaluate to be within the range [1, 3] but was -4");
 
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 7, column 5: " +
+        }, IllegalStateException.class, "Parsing failed at line 7, column 5: " +
             "vertex index must evaluate to be within the range [1, 3] but was 0");
 
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 8, column 3: " +
+        }, IllegalStateException.class, "Parsing failed at line 8, column 3: " +
             "vertex index must evaluate to be within the range [1, 3] but was 4");
     }
 
     @Test
-    void testReadFace_invalidTextureIndex() throws IOException {
+    void testReadFace_invalidTextureIndex() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "# test content",
@@ -473,30 +472,30 @@ class PolygonObjParserTest {
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 5, column 5: " +
+        }, IllegalStateException.class, "Parsing failed at line 5, column 5: " +
             "texture index cannot be used because no values of that type have been \
defined");  
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 9, column 13: " +
+        }, IllegalStateException.class, "Parsing failed at line 9, column 13: " +
             "texture index must evaluate to be within the range [1, 3] but was -4");
 
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 10, column 9: " +
+        }, IllegalStateException.class, "Parsing failed at line 10, column 9: " +
             "texture index must evaluate to be within the range [1, 3] but was 0");
 
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 11, column 5: " +
+        }, IllegalStateException.class, "Parsing failed at line 11, column 5: " +
             "texture index must evaluate to be within the range [1, 3] but was 4");
     }
 
     @Test
-    void testReadFace_invalidNormalIndex() throws IOException {
+    void testReadFace_invalidNormalIndex() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "# test content",
@@ -516,30 +515,30 @@ class PolygonObjParserTest {
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 5, column 6: " +
+        }, IllegalStateException.class, "Parsing failed at line 5, column 6: " +
             "normal index cannot be used because no values of that type have been \
defined");  
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 9, column 16: " +
+        }, IllegalStateException.class, "Parsing failed at line 9, column 16: " +
             "normal index must evaluate to be within the range [1, 3] but was -4");
 
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 10, column 11: " +
+        }, IllegalStateException.class, "Parsing failed at line 10, column 11: " +
             "normal index must evaluate to be within the range [1, 3] but was 0");
 
         nextFace(p);
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             p.readFace();
-        }, IOException.class, "Parsing failed at line 11, column 6: " +
+        }, IllegalStateException.class, "Parsing failed at line 11, column 6: " +
             "normal index must evaluate to be within the range [1, 3] but was 4");
     }
 
     @Test
-    void testParse() throws IOException {
+    void testParse() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "# test content",
@@ -627,7 +626,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testFace_getDefinedCompositeNormal() throws IOException {
+    void testFace_getDefinedCompositeNormal() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "v 0 0 0",
@@ -675,7 +674,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testFace_computeNormalFromVertices() throws IOException {
+    void testFace_computeNormalFromVertices() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "v 0 0 0",
@@ -706,7 +705,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testFace_getVertexAttributesCounterClockwise() throws IOException {
+    void testFace_getVertexAttributesCounterClockwise() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "v 0 0 0",
@@ -742,7 +741,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testFace_getVertices() throws IOException {
+    void testFace_getVertices() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "v 0 0 0",
@@ -770,7 +769,7 @@ class PolygonObjParserTest {
     }
 
     @Test
-    void testFace_getVerticesCounterClockwise() throws IOException {
+    void testFace_getVerticesCounterClockwise() {
         // arrange
         final PolygonObjParser p = parser(lines(
                 "v 0 0 0",
@@ -821,11 +820,11 @@ class PolygonObjParserTest {
         return sb.toString();
     }
 
-    private static void nextFace(final PolygonObjParser parser) throws IOException {
+    private static void nextFace(final PolygonObjParser parser) {
         nextMatchingKeyword(ObjConstants.FACE_KEYWORD, parser);
     }
 
-    private static void nextMatchingKeyword(final String keyword, final \
PolygonObjParser parser) throws IOException { +    private static void \
nextMatchingKeyword(final String keyword, final PolygonObjParser parser) {  while \
(parser.nextKeyword()) {  if (keyword.equals(parser.getCurrentKeyword())) {
                 return;
@@ -833,7 +832,7 @@ class PolygonObjParserTest {
         }
     }
 
-    private static void assertNextKeyword(final String expected, final \
PolygonObjParser parser) throws IOException { +    private static void \
assertNextKeyword(final String expected, final PolygonObjParser parser) {  \
Assertions.assertEquals(expected != null, parser.nextKeyword());  \
Assertions.assertEquals(expected, parser.getCurrentKeyword());  }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReaderTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReaderTest.java
 index 4a8840c..3e58e39 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReaderTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlFacetDefinitionReaderTest.java
 @@ -19,6 +19,8 @@ package org.apache.commons.geometry.io.euclidean.threed.stl;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -120,7 +122,7 @@ class BinaryStlFacetDefinitionReaderTest {
             // act/assert
             GeometryTestUtils.assertThrowsWithMessage(
                     () -> reader.getHeader(),
-                    IOException.class, "Failed to read STL header: data not \
available"); +                    IllegalStateException.class, "Failed to read STL \
header: data not available");  }
     }
 
@@ -133,7 +135,25 @@ class BinaryStlFacetDefinitionReaderTest {
             // act/assert
             GeometryTestUtils.assertThrowsWithMessage(
                     () -> reader.getHeader(),
-                    IOException.class, "Failed to read STL triangle count: data not \
available"); +                    IllegalStateException.class, "Failed to read STL \
triangle count: data not available"); +        }
+    }
+
+    @Test
+    void testGetHeader_ioException() throws IOException {
+        // arrange
+        final InputStream failIn = new InputStream() {
+            @Override
+            public int read() throws IOException {
+                throw new IOException("read");
+            }
+        };
+
+        try (BinaryStlFacetDefinitionReader reader = new \
BinaryStlFacetDefinitionReader(failIn)) { +            // act/assert
+            GeometryTestUtils.assertThrowsWithMessage(
+                    () -> reader.getHeader(),
+                    UncheckedIOException.class, "IOException: read");
         }
     }
 
@@ -147,7 +167,7 @@ class BinaryStlFacetDefinitionReaderTest {
             // act/assert
             GeometryTestUtils.assertThrowsWithMessage(
                     () -> reader.readFacet(),
-                    IOException.class, "Failed to read STL triangle at index 0: data \
not available"); +                    IllegalStateException.class, "Failed to read \
STL triangle at index 0: data not available");  }
     }
 
@@ -249,7 +269,7 @@ class BinaryStlFacetDefinitionReaderTest {
         return result;
     }
 
-    private static byte[] getBytes(final Vector3D vec) throws IOException {
+    private static byte[] getBytes(final Vector3D vec) {
         final byte[] result = new byte[Float.BYTES * 3];
         int offset = 0;
 
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriterTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriterTest.java
 index 5503978..3a1627b 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriterTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/BinaryStlWriterTest.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -36,7 +35,7 @@ class BinaryStlWriterTest {
     private final ByteArrayOutputStream out = new ByteArrayOutputStream();
 
     @Test
-    void testWriteHeader_nullHeaderContent() throws IOException {
+    void testWriteHeader_nullHeaderContent() {
         // act
         try (BinaryStlWriter writer = new BinaryStlWriter(out)) {
             writer.writeHeader(null, Short.MAX_VALUE);
@@ -51,7 +50,7 @@ class BinaryStlWriterTest {
     }
 
     @Test
-    void testWriteHeader_givenHeaderContent() throws IOException {
+    void testWriteHeader_givenHeaderContent() {
         // arrange
         final byte[] headerContent = new byte[StlConstants.BINARY_HEADER_BYTES];
         Arrays.fill(headerContent, (byte) 1);
@@ -70,7 +69,7 @@ class BinaryStlWriterTest {
     }
 
     @Test
-    void testWriteHeader_givenHeaderContentExceedsMaxLength() throws IOException {
+    void testWriteHeader_givenHeaderContentExceedsMaxLength() {
         // arrange
         final byte[] headerContent = new byte[2 * StlConstants.BINARY_HEADER_BYTES];
         Arrays.fill(headerContent, (byte) 1);
@@ -89,7 +88,7 @@ class BinaryStlWriterTest {
     }
 
     @Test
-    void testWriteFacet() throws IOException {
+    void testWriteFacet() {
         // arrange
         try (BinaryStlWriter writer = new BinaryStlWriter(out)) {
             writer.writeHeader(null, 2);
@@ -143,7 +142,7 @@ class BinaryStlWriterTest {
     }
 
     @Test
-    void testWriteFacet_ordersFacetCounterClockwise() throws IOException {
+    void testWriteFacet_ordersFacetCounterClockwise() {
         // arrange
         try (BinaryStlWriter writer = new BinaryStlWriter(out)) {
             writer.writeHeader(null, 2);
@@ -196,7 +195,7 @@ class BinaryStlWriterTest {
     }
 
     @Test
-    void testWriteFacet_invalidNormalGiven() throws IOException {
+    void testWriteFacet_invalidNormalGiven() {
         // arrange
         try (BinaryStlWriter writer = new BinaryStlWriter(out)) {
             writer.writeHeader(null, 3);
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3DTest.java
 index ba329c4..d45ab10 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryReadHandler3DTest.java
 @@ -88,7 +88,7 @@ class StlBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testRead_usesInputCharset() throws IOException {
+    void testRead_usesInputCharset() {
         // arrange
         final String content = "solid test\n" +
                 "facet normal 1 2 3 " +
@@ -111,7 +111,7 @@ class StlBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testRead_setDefaultCharset() throws IOException {
+    void testRead_setDefaultCharset() {
         // arrange
         final String content = "solid test\n" +
                 "facet normal 1 2 3 " +
@@ -137,7 +137,7 @@ class StlBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testRead_incorrectCharset() throws IOException {
+    void testRead_incorrectCharset() {
         // arrange
         final String content = "solid test\n" +
                 "facet normal 1 2 3 " +
@@ -157,34 +157,33 @@ class StlBoundaryReadHandler3DTest {
             Assertions.assertNotNull(reader.readFacet());
             Assertions.assertNotNull(reader.readFacet());
 
-            Assertions.assertThrows(IOException.class, () -> reader.readFacet());
+            Assertions.assertThrows(IllegalStateException.class, () -> \
reader.readFacet());  }
     }
 
     @Test
-    void testRead_notEnoughBytes() throws IOException {
+    void testRead_notEnoughBytes() {
         // arrange
         final ByteArrayInputStream in = new ByteArrayInputStream(new byte[1]);
         final GeometryInput input = new StreamGeometryInput(in);
 
         // act/assert
-        Assertions.assertThrows(IOException.class, () -> \
handler.facetDefinitionReader(input)); +        \
Assertions.assertThrows(IllegalStateException.class, () -> \
handler.facetDefinitionReader(input));  }
 
     @Test
-    void testRead_closesInputOnReaderCreationFailure() throws IOException {
+    void testRead_closesInputOnReaderCreationFailure() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(new \
ByteArrayInputStream(new byte[1]));  final GeometryInput input = new \
StreamGeometryInput(in);  
         // act/assert
-        Assertions.assertThrows(IOException.class, () -> \
handler.facetDefinitionReader(input)); +        \
Assertions.assertThrows(IllegalStateException.class, () -> \
handler.facetDefinitionReader(input));  
         Assertions.assertEquals(1, in.getCloseCount());
     }
 
-    private static BoundarySource3D boundariesToBoundarySource(final Stream<? \
                extends PlaneConvexSubset> boundaries)
-            throws IOException {
+    private static BoundarySource3D boundariesToBoundarySource(final Stream<? \
extends PlaneConvexSubset> boundaries) {  try (Stream<? extends PlaneConvexSubset> \
                toClose = boundaries) {
             return new BoundaryList3D(boundaries.collect(Collectors.toList()));
         }
@@ -199,7 +198,7 @@ class StlBoundaryReadHandler3DTest {
         }
     }
 
-    private static BoundarySource3D readerToBoundarySource(final \
FacetDefinitionReader reader) throws IOException { +    private static \
                BoundarySource3D readerToBoundarySource(final FacetDefinitionReader \
                reader) {
         return facetsToBoundarySource(EuclideanIOTestUtils.readAll(reader).stream());
  }
 }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3DTest.java
 index 58a3c99..7ad3613 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3DTest.java
 @@ -19,21 +19,26 @@ package org.apache.commons.geometry.io.euclidean.threed.stl;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.threed.BoundaryList3D;
 import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
+import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
 import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
 import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
 import org.apache.commons.geometry.io.core.input.GeometryInput;
 import org.apache.commons.geometry.io.core.input.StreamGeometryInput;
+import org.apache.commons.geometry.io.core.output.GeometryOutput;
 import org.apache.commons.geometry.io.core.output.StreamGeometryOutput;
 import org.apache.commons.geometry.io.euclidean.EuclideanIOTestUtils;
 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
@@ -82,7 +87,7 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite_boundarySource_empty() throws IOException {
+    void testWrite_boundarySource_empty() {
         // arrange
         final BoundarySource3D src = BoundarySource3D.of();
 
@@ -94,7 +99,7 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite_boundaryList() throws IOException {
+    void testWrite_boundaryList() {
         // arrange
         final BoundarySource3D src = \
EuclideanIOTestUtils.cubeMinusSphere(TEST_PRECISION);  
@@ -106,7 +111,7 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite_triangleMesh() throws IOException {
+    void testWrite_triangleMesh() {
         // arrange
         final TriangleMesh mesh = \
                EuclideanIOTestUtils.cubeMinusSphere(TEST_PRECISION)
                 .toTriangleMesh(TEST_PRECISION);
@@ -119,7 +124,7 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWrite_triangleMesh_empty() throws IOException {
+    void testWrite_triangleMesh_empty() {
         // arrange
         final TriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
                 .build();
@@ -132,7 +137,31 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets_list() throws IOException {
+    void testWriteStream_ioException() {
+        // arrange
+        final Stream<PlaneConvexSubset> stream = \
EuclideanIOTestUtils.cubeMinusSphere(TEST_PRECISION).boundaryStream(); +        final \
OutputStream failOut = new OutputStream() { +            @Override
+            public void write(final int b) throws IOException {
+                // do nothing
+            }
+
+            @Override
+            public void close() throws IOException {
+                throw new IOException("close");
+            }
+        };
+        final GeometryOutput output = new StreamGeometryOutput(failOut);
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(
+                () -> handler.write(stream, output),
+                UncheckedIOException.class,
+                "IOException: close");
+    }
+
+    @Test
+    void testWriteFacets_list() {
         // arrange
         final List<FacetDefinition> facets = cubeFacets();
 
@@ -144,7 +173,7 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets_list_empty() throws IOException {
+    void testWriteFacets_list_empty() {
         // act
         handler.writeFacets(Collections.emptyList(), new StreamGeometryOutput(out));
 
@@ -153,7 +182,7 @@ class StlBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets_includesStlFacetAttribute() throws IOException {
+    void testWriteFacets_includesStlFacetAttribute() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, \
0, 0), Vector3D.of(0, 1, 0));  final Vector3D normal = Vector3D.Unit.PLUS_Z;
@@ -174,7 +203,7 @@ class StlBoundaryWriteHandler3DTest {
         Assertions.assertEquals(attr, result.getAttributeValue());
     }
 
-    private BoundaryList3D readOutput() throws IOException {
+    private BoundaryList3D readOutput() {
         final GeometryInput input = new StreamGeometryInput(new \
ByteArrayInputStream(out.toByteArray()));  
         final StlBoundaryReadHandler3D readHandler = new StlBoundaryReadHandler3D();
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReadersTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReadersTest.java
 index a1d0e7e..35d0bce 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReadersTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlFacetDefinitionReadersTest.java
 @@ -75,7 +75,7 @@ class StlFacetDefinitionReadersTest {
     }
 
     @Test
-    void testCreate_nonStandardCharset_charsetGiven() throws IOException {
+    void testCreate_nonStandardCharset_charsetGiven() {
         // arrange
         final String content = "solid test\n" +
                 "facet normal 1 2 3 " +
@@ -101,7 +101,7 @@ class StlFacetDefinitionReadersTest {
     }
 
     @Test
-    void testCreate_nonStandardCharset_noCharsetGiven() throws IOException {
+    void testCreate_nonStandardCharset_noCharsetGiven() {
         // arrange
         final String content = "solid test\n" +
                 "facet normal 1 2 3 " +
@@ -123,7 +123,7 @@ class StlFacetDefinitionReadersTest {
 
             Assertions.assertNotNull(reader.readFacet());
             Assertions.assertNotNull(reader.readFacet());
-            Assertions.assertThrows(IOException.class, () -> reader.readFacet());
+            Assertions.assertThrows(IllegalStateException.class, () -> \
reader.readFacet());  }
     }
 
@@ -136,6 +136,7 @@ class StlFacetDefinitionReadersTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(
                 () -> StlFacetDefinitionReaders.create(in, null),
-                IOException.class, "Cannot determine STL format: attempted to read 5 \
bytes but found only 1 available"); +                IllegalStateException.class,
+                "Cannot determine STL format: attempted to read 5 bytes but found \
only 1 available");  }
 }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReaderTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReaderTest.java
 index 08c8e09..19255ce 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReaderTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReaderTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.util.Arrays;
 import java.util.List;
@@ -35,7 +34,7 @@ class TextStlFacetDefinitionReaderTest {
     private static final double TEST_EPS = 1e-10;
 
     @Test
-    void testGetSolidName() throws IOException {
+    void testGetSolidName() {
         // act/assert
         Assertions.assertEquals("Test Name", facetReader("solid    Test Name  \
                \r\n").getSolidName());
         Assertions.assertEquals("Test", facetReader("solid    Test  \
").getSolidName()); @@ -44,7 +43,7 @@ class TextStlFacetDefinitionReaderTest {
     }
 
     @Test
-    void testClose() throws IOException {
+    void testClose() {
         // arrange
         final CloseCountReader countReader = new CloseCountReader(new \
                StringReader(""));
         final TextStlFacetDefinitionReader reader = new \
TextStlFacetDefinitionReader(countReader); @@ -57,7 +56,7 @@ class \
TextStlFacetDefinitionReaderTest {  }
 
     @Test
-    void testEmpty() throws IOException {
+    void testEmpty() {
         // arrange
         final TextStlFacetDefinitionReader reader = facetReader(
                 "solid \n" +
@@ -74,7 +73,7 @@ class TextStlFacetDefinitionReaderTest {
     }
 
     @Test
-    void testSingleFacet() throws IOException {
+    void testSingleFacet() {
         // arrange
         final TextStlFacetDefinitionReader reader = facetReader(
                 "solid test\n" +
@@ -104,7 +103,7 @@ class TextStlFacetDefinitionReaderTest {
     }
 
     @Test
-    void testMultipleFacets() throws IOException {
+    void testMultipleFacets() {
         // arrange
         final TextStlFacetDefinitionReader reader = facetReader(
                 "solid test solid\r\n\n" +
@@ -158,7 +157,7 @@ class TextStlFacetDefinitionReaderTest {
     }
 
     @Test
-    void testNoName() throws IOException {
+    void testNoName() {
         // arrange
         final TextStlFacetDefinitionReader reader = facetReader(
                 "solid\n" +
@@ -188,7 +187,7 @@ class TextStlFacetDefinitionReaderTest {
     }
 
     @Test
-    void testContentEndsEarly() throws IOException {
+    void testContentEndsEarly() {
         // arrange
         final TextStlFacetDefinitionReader reader = facetReader(
                 "solid test\n" +
@@ -217,7 +216,7 @@ class TextStlFacetDefinitionReaderTest {
     }
 
     @Test
-    void testParseErrors() throws IOException {
+    void testParseErrors() {
         // act/assert
         assertParseError(
                 "soli test\n" +
@@ -248,7 +247,7 @@ class TextStlFacetDefinitionReaderTest {
     private static void assertParseError(final String content) {
         GeometryTestUtils.assertThrowsWithMessage(
                 () -> EuclideanIOTestUtils.readAll(facetReader(content)),
-                IOException.class,
+                IllegalStateException.class,
                 Pattern.compile("^Parsing failed.*"));
     }
 }
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriterTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriterTest.java
 index 8cf53d9..8714d09 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriterTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriterTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.stl;
 
-import java.io.IOException;
 import java.io.StringWriter;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
@@ -46,7 +45,7 @@ class TextStlWriterTest {
     private final StringWriter out = new StringWriter();
 
     @Test
-    void testDefaultProperties() throws IOException {
+    void testDefaultProperties() {
         // act/assert
         try (TextStlWriter writer = new TextStlWriter(out)) {
             Assertions.assertNotNull(writer.getDoubleFormat());
@@ -55,7 +54,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testNoContent() throws IOException {
+    void testNoContent() {
         // arrange
         final CloseCountWriter countWriter = new CloseCountWriter(out);
 
@@ -70,7 +69,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testStartSolid_alreadyStarted() throws IOException {
+    void testStartSolid_alreadyStarted() {
         // arrange
         try (TextStlWriter writer = new TextStlWriter(out)) {
             writer.startSolid();
@@ -83,7 +82,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testEndSolid_notStarted() throws IOException {
+    void testEndSolid_notStarted() {
         // arrange
         try (TextStlWriter writer = new TextStlWriter(out)) {
             // act/assert
@@ -94,7 +93,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testEmpty_noName() throws IOException {
+    void testEmpty_noName() {
         // arrange
         final CloseCountWriter countWriter = new CloseCountWriter(out);
 
@@ -114,7 +113,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testEmpty_withName() throws IOException {
+    void testEmpty_withName() {
         // arrange
         final CloseCountWriter countWriter = new CloseCountWriter(out);
 
@@ -134,7 +133,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testClose_endsSolid() throws IOException {
+    void testClose_endsSolid() {
         // arrange
         final CloseCountWriter countWriter = new CloseCountWriter(out);
 
@@ -153,7 +152,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testStartSolid_containsNewLine() throws IOException {
+    void testStartSolid_containsNewLine() {
         // arrange
         try (TextStlWriter writer = new TextStlWriter(out)) {
             final String err = "Solid name cannot contain new line characters";
@@ -172,7 +171,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWriteTriangle_noNormal_computesNormal() throws IOException {
+    void testWriteTriangle_noNormal_computesNormal() {
         // arrange
         final Vector3D p1 = Vector3D.of(0, 4, 0);
         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
@@ -198,7 +197,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWriteTriangle_zeroNormal_computesNormal() throws IOException {
+    void testWriteTriangle_zeroNormal_computesNormal() {
         // arrange
         final Vector3D p1 = Vector3D.of(0, 4, 0);
         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
@@ -224,7 +223,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWriteTriangle_noNormal_cannotComputeNormal() throws IOException {
+    void testWriteTriangle_noNormal_cannotComputeNormal() {
         // arrange
         final Vector3D p1 = Vector3D.ZERO;
         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
@@ -250,7 +249,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWriteTriangle_withNormal_correctOrientation() throws IOException {
+    void testWriteTriangle_withNormal_correctOrientation() {
         // arrange
         final Vector3D p1 = Vector3D.of(0, 4, 0);
         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
@@ -278,7 +277,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWriteTriangle_withNormal_reversedOrientation() throws IOException {
+    void testWriteTriangle_withNormal_reversedOrientation() {
         // arrange
         final Vector3D p1 = Vector3D.of(0, 4, 0);
         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
@@ -306,7 +305,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_verticesAndNormal() throws IOException {
+    void testWrite_verticesAndNormal() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
@@ -350,7 +349,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_verticesAndNormal_moreThanThreeVertices() throws IOException {
+    void testWrite_verticesAndNormal_moreThanThreeVertices() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), \
Vector3D.of(0, 1, 0)); @@ -384,7 +383,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_verticesAndNormal_fewerThanThreeVertices() throws IOException {
+    void testWrite_verticesAndNormal_fewerThanThreeVertices() {
         // arrange
         try (TextStlWriter writer = new TextStlWriter(out)) {
             writer.startSolid();
@@ -404,7 +403,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_boundary() throws IOException {
+    void testWrite_boundary() {
         // arrange
         final ConvexPolygon3D boundary = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 0, \
1), Vector3D.of(0, 0, 1)), @@ -438,7 +437,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_facetDefinition_noNormal() throws IOException {
+    void testWrite_facetDefinition_noNormal() {
         // arrange
         final FacetDefinition facet = new SimpleFacetDefinition(Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), \
Vector3D.of(0, 1, 0))); @@ -471,7 +470,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_facetDefinition_withNormal() throws IOException {
+    void testWrite_facetDefinition_withNormal() {
         // arrange
         final Vector3D normal = Vector3D.Unit.PLUS_Z;
         final FacetDefinition facet = new SimpleFacetDefinition(Arrays.asList(
@@ -506,7 +505,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_noSolidStarted() throws IOException {
+    void testWrite_noSolidStarted() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
@@ -536,7 +535,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_customFormat() throws IOException {
+    void testWrite_customFormat() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(1.0 / 3.0, 0, 0), Vector3D.of(0, 1.0 / \
3.0, 0)); @@ -569,7 +568,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_badFacet_withNormal() throws IOException {
+    void testWrite_badFacet_withNormal() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(
                 Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
@@ -595,7 +594,7 @@ class TextStlWriterTest {
     }
 
     @Test
-    void testWrite_badFacet_noNormal() throws IOException {
+    void testWrite_badFacet_noNormal() {
         // arrange
         final List<Vector3D> vertices = Arrays.asList(
                 Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3DTest.java
 index 77d4ee5..3585532 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryReadHandler3DTest.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -48,7 +47,7 @@ class TextBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader() throws IOException {
+    void testFacetDefinitionReader() {
         // arrange
         final InputStream in = input("0 0 0; 1 1 0; 0 1 0", StandardCharsets.UTF_8);
 
@@ -64,7 +63,7 @@ class TextBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_usesInputCharset() throws IOException {
+    void testFacetDefinitionReader_usesInputCharset() {
         // arrange
         final InputStream in = input("0 0 0; 1 1 0; 0 1 0", \
StandardCharsets.UTF_16);  
@@ -80,7 +79,7 @@ class TextBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_setDefaultCharset() throws IOException {
+    void testFacetDefinitionReader_setDefaultCharset() {
         // arrange
         handler.setDefaultCharset(StandardCharsets.UTF_16);
         final InputStream in = input("0 0 0; 1 1 0; 0 1 0", \
StandardCharsets.UTF_16); @@ -97,7 +96,7 @@ class TextBoundaryReadHandler3DTest {
     }
 
     @Test
-    void testFacetDefinitionReader_close() throws IOException {
+    void testFacetDefinitionReader_close() {
         // arrange
         final CloseCountInputStream in = new CloseCountInputStream(input("", \
StandardCharsets.UTF_8));  
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3DTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3DTest.java
 index 8b6e21f..f29ba3d 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3DTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextBoundaryWriteHandler3DTest.java
 @@ -17,7 +17,6 @@
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
@@ -73,7 +72,7 @@ class TextBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets() throws IOException {
+    void testWriteFacets() {
         // arrange
         final TextBoundaryWriteHandler3D handler = new TextBoundaryWriteHandler3D();
         final CloseCountOutputStream closeOut = new CloseCountOutputStream(out);
@@ -88,7 +87,7 @@ class TextBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets_usesOutputCharset() throws IOException {
+    void testWriteFacets_usesOutputCharset() {
         // arrange
         final TextBoundaryWriteHandler3D handler = new TextBoundaryWriteHandler3D();
         final CloseCountOutputStream closeOut = new CloseCountOutputStream(out);
@@ -103,7 +102,7 @@ class TextBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteFacets_customConfiguration() throws IOException {
+    void testWriteFacets_customConfiguration() {
         // arrange
         final DecimalFormat fmt =
                 new DecimalFormat("0.0", \
DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @@ -128,7 +127,7 @@ class \
TextBoundaryWriteHandler3DTest {  }
 
     @Test
-    void testWriteBoundarySource() throws IOException {
+    void testWriteBoundarySource() {
         // arrange
         final TextBoundaryWriteHandler3D handler = new TextBoundaryWriteHandler3D();
         final CloseCountOutputStream closeOut = new CloseCountOutputStream(out);
@@ -143,7 +142,7 @@ class TextBoundaryWriteHandler3DTest {
     }
 
     @Test
-    void testWriteBoundarySource_customConfiguration() throws IOException {
+    void testWriteBoundarySource_customConfiguration() {
         // arrange
         // arrange
         final DecimalFormat fmt =
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReaderTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReaderTest.java
 index 15f2825..b4cb9fb 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReaderTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionReaderTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.util.Arrays;
 import java.util.List;
@@ -59,7 +58,7 @@ class TextFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_empty() throws IOException {
+    void testReadFacet_empty() {
         // arrange
         TextFacetDefinitionReader reader = facetReader("");
 
@@ -71,7 +70,7 @@ class TextFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_singleFacet() throws IOException {
+    void testReadFacet_singleFacet() {
         // arrange
         TextFacetDefinitionReader reader = facetReader(
                 "1.0 2.0 3.0 40 50 60 7.0e-2 8e-2 9E-02 1.01e+1 -11.02 +12");
@@ -90,7 +89,7 @@ class TextFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_multipleFacets() throws IOException {
+    void testReadFacet_multipleFacets() {
         // arrange
         TextFacetDefinitionReader reader = facetReader(
                 "1,2,3    4,5,6 7,8,9    10,11,12\r" +
@@ -123,7 +122,7 @@ class TextFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_blankLinesAndComments() throws IOException {
+    void testReadFacet_blankLinesAndComments() {
         // arrange
         TextFacetDefinitionReader reader = facetReader(
                 "# some ignored numbers: 1 2 3 4 5 6\n" +
@@ -158,7 +157,7 @@ class TextFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_nonDefaultCommentToken() throws IOException {
+    void testReadFacet_nonDefaultCommentToken() {
         // arrange
         TextFacetDefinitionReader reader = facetReader(
                 "5$ some ignored numbers: 1 2 3 4 5 6\n" +
@@ -195,7 +194,7 @@ class TextFacetDefinitionReaderTest {
     }
 
     @Test
-    void testReadFacet_longCommentToken() throws IOException {
+    void testReadFacet_longCommentToken() {
         // arrange
         TextFacetDefinitionReader reader = facetReader(
                 "this_is-a-comment some ignored numbers: 1 2 3 4 5 6\n" +
@@ -240,7 +239,7 @@ class TextFacetDefinitionReaderTest {
         // act
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 1: expected double but found empty \
token followed by [#]");  }
 
@@ -253,7 +252,7 @@ class TextFacetDefinitionReaderTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 1: expected double but found empty \
token followed by [#]");  }
 
@@ -265,7 +264,7 @@ class TextFacetDefinitionReaderTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 3: expected double but found \
[abc]");  }
 
@@ -281,27 +280,27 @@ class TextFacetDefinitionReaderTest {
         // act/assert
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 1, column 2: expected double but found end \
of line");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 2, column 4: expected double but found end \
of line");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 3, column 6: expected double but found end \
of line");  
         GeometryTestUtils.assertThrowsWithMessage(() -> {
             reader.readFacet();
-        }, IOException.class,
+        }, IllegalStateException.class,
                 "Parsing failed at line 4, column 15: expected double but found end \
of line");  }
 
     @Test
-    void testClose() throws IOException {
+    void testClose() {
         // arrange
         final CloseCountReader countReader = new CloseCountReader(new \
                StringReader(""));
         final TextFacetDefinitionReader reader = new \
                TextFacetDefinitionReader(countReader);
diff --git a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriterTest.java \
b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriterTest.java
 index 8d0b516..7f501d5 100644
--- a/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriterTest.java
                
+++ b/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/txt/TextFacetDefinitionWriterTest.java
 @@ -16,7 +16,6 @@
  */
 package org.apache.commons.geometry.io.euclidean.threed.txt;
 
-import java.io.IOException;
 import java.io.StringWriter;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
@@ -110,7 +109,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteComment() throws IOException {
+    void testWriteComment() {
         // arrange
         fdWriter.setCommentToken("-- ");
         fdWriter.setLineSeparator("\r\n");
@@ -129,7 +128,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteComment_noCommentToken() throws IOException {
+    void testWriteComment_noCommentToken() {
         // arrange
         fdWriter.setCommentToken(null);
 
@@ -140,7 +139,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteBlankLine() throws IOException {
+    void testWriteBlankLine() {
         // act
         fdWriter.writeBlankLine();
         fdWriter.setLineSeparator("\r");
@@ -151,7 +150,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteVertices() throws IOException {
+    void testWriteVertices() {
         // arrange
         final List<Vector3D> vertices1 = Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(0.5, 0, 0), Vector3D.of(0, -0.5, 0));
@@ -169,7 +168,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteVertices_invalidCount() throws IOException {
+    void testWriteVertices_invalidCount() {
         // arrange
         fdWriter.setFacetVertexCount(4);
         final List<Vector3D> notEnough = Arrays.asList(Vector3D.ZERO, \
Vector3D.Unit.PLUS_X); @@ -188,7 +187,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteFacetDefinition() throws IOException {
+    void testWriteFacetDefinition() {
         // arrange
         final DecimalFormat fmt =
                 new DecimalFormat("0.0##", \
DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @@ -211,7 +210,7 @@ class \
TextFacetDefinitionWriterTest {  }
 
     @Test
-    void testWriteFacetDefinition_invalidCount() throws IOException {
+    void testWriteFacetDefinition_invalidCount() {
         // arrange
         fdWriter.setFacetVertexCount(4);
         final SimpleFacetDefinition tooMany = new \
SimpleFacetDefinition(Arrays.asList( @@ -225,7 +224,7 @@ class \
TextFacetDefinitionWriterTest {  }
 
     @Test
-    void testWritePlaneConvexSubset() throws IOException {
+    void testWritePlaneConvexSubset() {
         // arrange
         final ConvexPolygon3D poly1 = \
                Planes.convexPolygonFromVertices(Arrays.asList(
                     Vector3D.ZERO, Vector3D.of(0, 0, -0.5), Vector3D.of(0, -0.5, 0)
@@ -245,7 +244,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWritePlaneConvexSubset_convertsToTriangles() throws IOException {
+    void testWritePlaneConvexSubset_convertsToTriangles() {
         // arrange
         final ConvexPolygon3D poly = Planes.convexPolygonFromVertices(Arrays.asList(
                     Vector3D.ZERO, Vector3D.of(0, 1, 0), Vector3D.of(0, 1, 1), \
Vector3D.of(0, 0, 1) @@ -263,7 +262,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWritePlaneConvexSubset_infinite() throws IOException {
+    void testWritePlaneConvexSubset_infinite() {
         // arrange
         final PlaneConvexSubset inf = Planes.fromNormal(Vector3D.Unit.PLUS_X, \
TEST_PRECISION).span();  
@@ -274,7 +273,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteBoundarySource() throws IOException {
+    void testWriteBoundarySource() {
         // arrange
         final ConvexPolygon3D poly1 = \
                Planes.convexPolygonFromVertices(Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(0, 0, -0.5), Vector3D.of(0, -0.5, 0)
@@ -293,7 +292,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteBoundarySource_empty() throws IOException {
+    void testWriteBoundarySource_empty() {
         // act
         fdWriter.write(BoundarySource3D.of(Collections.emptyList()));
 
@@ -302,7 +301,7 @@ class TextFacetDefinitionWriterTest {
     }
 
     @Test
-    void testWriteBoundarySource_alternativeFormatting() throws IOException {
+    void testWriteBoundarySource_alternativeFormatting() {
         // arrange
         final DecimalFormat fmt =
                 new DecimalFormat("0.0", \
DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @@ -335,7 +334,7 @@ class \
TextFacetDefinitionWriterTest {  }
 
     @Test
-    void testCsvFormat() throws IOException {
+    void testCsvFormat() {
         // arrange
         final ConvexPolygon3D poly1 = \
                Planes.convexPolygonFromVertices(Arrays.asList(
                 Vector3D.ZERO, Vector3D.of(0, 0, -0.5901), Vector3D.of(0, -0.501, 0)


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic