diff --git a/src/builtins/read.rs b/src/builtins/read.rs
index e7467287f..f34ba88c6 100644
--- a/src/builtins/read.rs
+++ b/src/builtins/read.rs
@@ -49,7 +49,7 @@ struct Options {
     silent: bool,
     split_null: bool,
     to_stdout: bool,
-    nchars: i32,
+    nchars: usize,
     one_line: bool,
 }
 
@@ -136,18 +136,17 @@ fn parse_cmd_opts(
             }
             'n' => {
                 opts.nchars = match fish_wcstoi(w.woptarg.unwrap()) {
-                    Ok(n) => n,
-                    Err(err) => {
-                        if err == wutil::Error::Overflow {
-                            streams.err.append(wgettext_fmt!(
-                                "%ls: Argument '%ls' is out of range\n",
-                                cmd,
-                                w.woptarg.unwrap()
-                            ));
-                            builtin_print_error_trailer(parser, streams.err, cmd);
-                            return Err(STATUS_INVALID_ARGS);
-                        }
-
+                    Ok(n) if n >= 0 => n.try_into().unwrap(),
+                    Err(wutil::Error::Overflow) => {
+                        streams.err.append(wgettext_fmt!(
+                            "%ls: Argument '%ls' is out of range\n",
+                            cmd,
+                            w.woptarg.unwrap()
+                        ));
+                        builtin_print_error_trailer(parser, streams.err, cmd);
+                        return Err(STATUS_INVALID_ARGS);
+                    }
+                    _ => {
                         streams.err.append(wgettext_fmt!(
                             BUILTIN_ERR_NOT_NUMBER,
                             cmd,
@@ -210,7 +209,7 @@ fn parse_cmd_opts(
 fn read_interactive(
     parser: &Parser,
     buff: &mut WString,
-    nchars: i32,
+    nchars: usize,
     shell: bool,
     silent: bool,
     prompt: &wstr,
@@ -253,12 +252,12 @@ fn read_interactive(
     };
     if let Some(line) = mline {
         *buff = line;
-        if nchars > 0 && usize::try_from(nchars).unwrap() < buff.len() {
+        if nchars > 0 && nchars < buff.len() {
             // Line may be longer than nchars if a keybinding used `commandline -i`
             // note: we're deliberately throwing away the tail of the commandline.
             // It shouldn't be unread because it was produced with `commandline -i`,
             // not typed.
-            buff.truncate(usize::try_from(nchars).unwrap());
+            buff.truncate(nchars);
         }
     } else {
         exit_res = STATUS_CMD_ERROR;
@@ -339,7 +338,7 @@ fn read_in_chunks(fd: RawFd, buff: &mut WString, split_null: bool, do_seek: bool
 fn read_one_char_at_a_time(
     fd: RawFd,
     buff: &mut WString,
-    nchars: i32,
+    nchars: usize,
     split_null: bool,
 ) -> Option<c_int> {
     let mut exit_res = STATUS_CMD_OK;
@@ -398,7 +397,7 @@ fn read_one_char_at_a_time(
         }
 
         buff.push(res);
-        if nchars > 0 && usize::try_from(nchars).unwrap() <= buff.len() {
+        if nchars > 0 && nchars <= buff.len() {
             break;
         }
     }
diff --git a/src/reader.rs b/src/reader.rs
index 173498e80..2dc9d3f95 100644
--- a/src/reader.rs
+++ b/src/reader.rs
@@ -890,13 +890,8 @@ pub fn reader_reading_interrupted() -> i32 {
 /// characters even if a full line has not yet been read. Note: the returned value may be longer
 /// than nchars if a single keypress resulted in multiple characters being inserted into the
 /// commandline.
-pub fn reader_readline(nchars: i32) -> Option<WString> {
-    let nchars = usize::try_from(nchars).unwrap();
-    let nchars = if nchars == 0 {
-        None
-    } else {
-        Some(NonZeroUsize::try_from(nchars).unwrap())
-    };
+pub fn reader_readline(nchars: usize) -> Option<WString> {
+    let nchars = NonZeroUsize::try_from(nchars).ok();
     let data = current_data().unwrap();
     // Apply any outstanding commandline changes (#8633).
     data.apply_commandline_state_changes();
diff --git a/tests/checks/read.fish b/tests/checks/read.fish
index 38528d590..b921b5600 100644
--- a/tests/checks/read.fish
+++ b/tests/checks/read.fish
@@ -393,3 +393,10 @@ echo read $status
 echo ' foo' | read -n 1 -la var
 set -S var
 #CHECK: $var: set in local scope, unexported, with 0 elements
+
+echo foo | read -n -1
+# CHECKERR: read: -1: invalid integer
+# CHECKERR: {{.*}}read.fish (line {{\d+}}):
+# CHECKERR: echo foo | read -n -1
+# CHECKERR: ^
+# CHECKERR: (Type 'help read' for related documentation)