diff --git a/src/binary/history.rs b/src/binary/history.rs
index 078def1f20045d3de7443bf6d6510d11aa8443e5..2b12df6ad5672203c6c60b008b6c571bfce367e6 100644
--- a/src/binary/history.rs
+++ b/src/binary/history.rs
@@ -103,7 +103,9 @@ impl<'a> InteractiveBinary<'a> {
             return false;
         }
 
-        if ignore.no_such_command && self.shell.borrow().previous_status() == NO_SUCH_COMMAND {
+        if ignore.no_such_command
+            && self.shell.borrow().previous_status() == Status::NO_SUCH_COMMAND
+        {
             return false;
         }
 
diff --git a/src/binary/mod.rs b/src/binary/mod.rs
index 26578da726cf732ec0810308d6b40cd6b51e374e..9a69d2b94db845b2e530e26ceb2bcf95f39a5a20 100644
--- a/src/binary/mod.rs
+++ b/src/binary/mod.rs
@@ -9,7 +9,7 @@ use self::{prompt::prompt, readln::readln};
 use ion_shell::{
     builtins::man_pages,
     parser::{Expander, Terminator},
-    status::{FAILURE, SUCCESS},
+    status::Status,
     Shell,
 };
 use itertools::Itertools;
@@ -114,34 +114,28 @@ impl<'a> InteractiveBinary<'a> {
     /// Liner.
     pub fn execute_interactive(self) -> ! {
         let context_bis = self.context.clone();
-        let history = &move |args: &[small::String], _shell: &mut Shell| -> i32 {
+        let history = &move |args: &[small::String], _shell: &mut Shell| -> Status {
             if man_pages::check_help(args, MAN_HISTORY) {
-                return SUCCESS;
+                return Status::SUCCESS;
             }
 
             print!("{}", context_bis.borrow().history.buffers.iter().format("\n"));
-            SUCCESS
+            Status::SUCCESS
         };
 
         let context_bis = self.context.clone();
-        let keybindings = &move |args: &[small::String], _shell: &mut Shell| -> i32 {
+        let keybindings = &move |args: &[small::String], _shell: &mut Shell| -> Status {
             match args.get(1).map(|s| s.as_str()) {
                 Some("vi") => {
                     context_bis.borrow_mut().key_bindings = KeyBindings::Vi;
-                    SUCCESS
+                    Status::SUCCESS
                 }
                 Some("emacs") => {
                     context_bis.borrow_mut().key_bindings = KeyBindings::Emacs;
-                    SUCCESS
-                }
-                Some(_) => {
-                    eprintln!("Invalid keybindings. Choices are vi and emacs");
-                    FAILURE
-                }
-                None => {
-                    eprintln!("keybindings need an argument");
-                    FAILURE
+                    Status::SUCCESS
                 }
+                Some(_) => Status::error("Invalid keybindings. Choices are vi and emacs"),
+                None => Status::error("keybindings need an argument"),
             }
         };
 
diff --git a/src/binary/readln.rs b/src/binary/readln.rs
index be1ee8b2e90bca04b2c99bcb81d6aea51be7d0f5..410532f5c62ce5ca67938b3b2351cd9adab1bf4b 100644
--- a/src/binary/readln.rs
+++ b/src/binary/readln.rs
@@ -22,7 +22,7 @@ pub fn readln(binary: &InteractiveBinary) -> Option<String> {
         Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => {
             let mut shell = binary.shell.borrow_mut();
             if !shell.unterminated && shell.exit_block().is_err() {
-                shell.exit(None);
+                shell.exit();
             }
             None
         }
diff --git a/src/lib/builtins/command_info.rs b/src/lib/builtins/command_info.rs
index 12d0b8e1570aa77532caceecc2b811611a3329d3..204ef8656c2ab83857404f825aab51e006e3745c 100644
--- a/src/lib/builtins/command_info.rs
+++ b/src/lib/builtins/command_info.rs
@@ -1,15 +1,15 @@
 use crate::{
     builtins::man_pages::*,
-    shell::{status::*, Shell, Value},
+    shell::{status::Status, Shell, Value},
     sys,
 };
 use small;
 
 use std::{borrow::Cow, env, path::Path};
 
-pub fn which(args: &[small::String], shell: &mut Shell) -> Result<i32, ()> {
+pub fn which(args: &[small::String], shell: &mut Shell) -> Result<Status, ()> {
     if check_help(args, MAN_WHICH) {
-        return Ok(SUCCESS);
+        return Ok(Status::SUCCESS);
     }
 
     if args.len() == 1 {
@@ -17,7 +17,7 @@ pub fn which(args: &[small::String], shell: &mut Shell) -> Result<i32, ()> {
         return Err(());
     }
 
-    let mut result = SUCCESS;
+    let mut result = Status::SUCCESS;
     for command in &args[1..] {
         match get_command_info(command, shell) {
             Ok(c_type) => match c_type.as_ref() {
@@ -30,20 +30,20 @@ pub fn which(args: &[small::String], shell: &mut Shell) -> Result<i32, ()> {
                 "builtin" => println!("{}: built-in shell command", command),
                 _path => println!("{}", _path),
             },
-            Err(_) => result = FAILURE,
+            Err(_) => result = Status::from_exit_code(1),
         }
     }
     Ok(result)
 }
 
-pub fn find_type(args: &[small::String], shell: &mut Shell) -> Result<i32, ()> {
+pub fn find_type(args: &[small::String], shell: &mut Shell) -> Result<Status, ()> {
     // Type does not accept help flags, aka "--help".
     if args.len() == 1 {
         eprintln!("type: Expected at least 1 args, got only 0");
         return Err(());
     }
 
-    let mut result = FAILURE;
+    let mut result = Status::SUCCESS;
     for command in &args[1..] {
         match get_command_info(command, shell) {
             Ok(c_type) => {
@@ -58,9 +58,8 @@ pub fn find_type(args: &[small::String], shell: &mut Shell) -> Result<i32, ()> {
                     "builtin" => println!("{} is a shell builtin", command),
                     _path => println!("{} is {}", command, _path),
                 }
-                result = SUCCESS;
             }
-            Err(_) => eprintln!("type: {}: not found", command),
+            Err(_) => result = Status::error(format!("type: {}: not found", command)),
         }
     }
     Ok(result)
diff --git a/src/lib/builtins/functions.rs b/src/lib/builtins/functions.rs
index db6943705cf7fa95ea7b31de29b5a1e53882de1b..adc601b40d148319f8094e912447895726f9a4ef 100644
--- a/src/lib/builtins/functions.rs
+++ b/src/lib/builtins/functions.rs
@@ -1,7 +1,7 @@
-use crate::shell::{status::*, variables::Variables};
+use crate::shell::{status::Status, variables::Variables};
 use std::io::{self, Write};
 
-pub fn print_functions(vars: &Variables) -> i32 {
+pub fn print_functions(vars: &Variables) -> Status {
     let stdout = io::stdout();
     let stdout = &mut stdout.lock();
     let _ = writeln!(stdout, "# Functions");
@@ -13,5 +13,5 @@ pub fn print_functions(vars: &Variables) -> i32 {
             let _ = writeln!(stdout, "    {}", fn_name);
         }
     }
-    SUCCESS
+    Status::SUCCESS
 }
diff --git a/src/lib/builtins/is.rs b/src/lib/builtins/is.rs
index 3cc785b657a64e56200869e32dc6fa7dda62f4d0..0fb7b8550d42c8c76a3960da392cc6f89b2a274c 100644
--- a/src/lib/builtins/is.rs
+++ b/src/lib/builtins/is.rs
@@ -1,24 +1,24 @@
-use crate::{shell::Shell, types};
+use crate::{shell::Shell, status::Status, types};
 use small;
 
-pub fn is(args: &[small::String], shell: &mut Shell) -> Result<(), String> {
+pub fn is(args: &[small::String], shell: &mut Shell) -> Status {
     match args.len() {
         4 => {
             if args[1] != "not" {
-                return Err(format!("Expected 'not' instead found '{}'\n", args[1]).to_string());
+                return Status::error(format!("Expected 'not' instead found '{}'", args[1]));
             } else if eval_arg(&*args[2], shell) == eval_arg(&*args[3], shell) {
-                return Err("".to_string());
+                return Status::error("");
             }
         }
         3 => {
             if eval_arg(&*args[1], shell) != eval_arg(&*args[2], shell) {
-                return Err("".to_string());
+                return Status::error("");
             }
         }
-        _ => return Err("is needs 3 or 4 arguments\n".to_string()),
+        _ => return Status::error("is needs 3 or 4 arguments"),
     }
 
-    Ok(())
+    Status::SUCCESS
 }
 
 fn eval_arg(arg: &str, shell: &mut Shell) -> types::Str {
@@ -49,30 +49,21 @@ fn test_is() {
     shell.variables_mut().set("y", "0");
 
     // Four arguments
-    assert_eq!(
-        is(&vec_string(&["is", " ", " ", " "]), &mut shell),
-        Err("Expected 'not' instead found ' '\n".to_string())
-    );
-    assert_eq!(is(&vec_string(&["is", "not", " ", " "]), &mut shell), Err("".to_string()));
-    assert_eq!(is(&vec_string(&["is", "not", "$x", "$x"]), &mut shell), Err("".to_string()));
-    assert_eq!(is(&vec_string(&["is", "not", "2", "1"]), &mut shell), Ok(()));
-    assert_eq!(is(&vec_string(&["is", "not", "$x", "$y"]), &mut shell), Ok(()));
+    assert!(is(&vec_string(&["is", " ", " ", " "]), &mut shell).is_failure());
+    assert!(is(&vec_string(&["is", "not", " ", " "]), &mut shell).is_failure());
+    assert!(is(&vec_string(&["is", "not", "$x", "$x"]), &mut shell).is_failure());
+    assert!(is(&vec_string(&["is", "not", "2", "1"]), &mut shell).is_success());
+    assert!(is(&vec_string(&["is", "not", "$x", "$y"]), &mut shell).is_success());
 
     // Three arguments
-    assert_eq!(is(&vec_string(&["is", "1", "2"]), &mut shell), Err("".to_string()));
-    assert_eq!(is(&vec_string(&["is", "$x", "$y"]), &mut shell), Err("".to_string()));
-    assert_eq!(is(&vec_string(&["is", " ", " "]), &mut shell), Ok(()));
-    assert_eq!(is(&vec_string(&["is", "$x", "$x"]), &mut shell), Ok(()));
+    assert!(is(&vec_string(&["is", "1", "2"]), &mut shell).is_failure());
+    assert!(is(&vec_string(&["is", "$x", "$y"]), &mut shell).is_failure());
+    assert!(is(&vec_string(&["is", " ", " "]), &mut shell).is_success());
+    assert!(is(&vec_string(&["is", "$x", "$x"]), &mut shell).is_success());
 
     // Two arguments
-    assert_eq!(
-        is(&vec_string(&["is", " "]), &mut shell),
-        Err("is needs 3 or 4 arguments\n".to_string())
-    );
+    assert!(is(&vec_string(&["is", " "]), &mut shell).is_failure());
 
     // One argument
-    assert_eq!(
-        is(&vec_string(&["is"]), &mut shell),
-        Err("is needs 3 or 4 arguments\n".to_string())
-    );
+    assert!(is(&vec_string(&["is"]), &mut shell).is_failure());
 }
diff --git a/src/lib/builtins/job_control.rs b/src/lib/builtins/job_control.rs
index 7ea8a41a1e774ad3cb62a1fc3cd9727081fb6bb3..c419450212318a6038ac1ca96cf1daffabdf6d47 100644
--- a/src/lib/builtins/job_control.rs
+++ b/src/lib/builtins/job_control.rs
@@ -73,34 +73,31 @@ pub fn jobs(shell: &mut Shell) {
 /// Hands control of the foreground process to the specified jobs, recording their exit status.
 /// If the job is stopped, the job will be resumed.
 /// If multiple jobs are given, then only the last job's exit status will be returned.
-pub fn fg(shell: &mut Shell, args: &[small::String]) -> i32 {
-    fn fg_job(shell: &mut Shell, njob: usize) -> i32 {
+pub fn fg(shell: &mut Shell, args: &[small::String]) -> Status {
+    fn fg_job(shell: &mut Shell, njob: usize) -> Status {
         if let Some(job) = shell.background_jobs().iter().nth(njob).filter(|p| p.exists()) {
             // Give the bg task the foreground, and wait for it to finish. Also resume it if it
             // isn't running
             shell.set_bg_task_in_foreground(job.pid(), !job.is_running())
         } else {
             // Informs the user that the specified job ID no longer exists.
-            eprintln!("ion: fg: job {} does not exist", njob);
-            return FAILURE;
+            return Status::error(format!("ion: fg: job {} does not exist", njob));
         }
     }
 
-    let mut status = 0;
+    let mut status = Status::SUCCESS;
     if args.is_empty() {
         status = if let Some(previous_job) = shell.previous_job() {
             fg_job(shell, previous_job)
         } else {
-            eprintln!("ion: fg: no jobs are running in the background");
-            FAILURE
+            Status::error(format!("ion: fg: no jobs are running in the background"))
         };
     } else {
         for arg in args {
             match arg.parse::<usize>() {
                 Ok(njob) => status = fg_job(shell, njob),
                 Err(_) => {
-                    eprintln!("ion: fg: {} is not a valid job number", arg);
-                    status = FAILURE;
+                    status = Status::error(format!("ion: fg: {} is not a valid job number", arg))
                 }
             }
         }
@@ -109,44 +106,37 @@ pub fn fg(shell: &mut Shell, args: &[small::String]) -> i32 {
 }
 
 /// Resumes a stopped background process, if it was stopped.
-pub fn bg(shell: &mut Shell, args: &[small::String]) -> i32 {
-    fn bg_job(shell: &mut Shell, njob: usize) -> bool {
+pub fn bg(shell: &mut Shell, args: &[small::String]) -> Status {
+    fn bg_job(shell: &mut Shell, njob: usize) -> Status {
         if let Some(job) = shell.background_jobs().iter().nth(njob).filter(|p| p.exists()) {
             if job.is_running() {
-                eprintln!("ion: bg: job {} is already running", njob);
-                false
+                Status::error(format!("ion: bg: job {} is already running", njob))
             } else {
                 job.resume();
-                true
+                Status::SUCCESS
             }
         } else {
-            eprintln!("ion: bg: job {} does not exist", njob);
-            false
+            Status::error(format!("ion: bg: job {} does not exist", njob))
         }
     }
 
     if args.is_empty() {
         if let Some(previous_job) = shell.previous_job() {
-            if bg_job(shell, previous_job) {
-                SUCCESS
-            } else {
-                FAILURE
-            }
+            bg_job(shell, previous_job)
         } else {
-            eprintln!("ion: bg: no jobs are running in the background");
-            FAILURE
+            Status::error(format!("ion: bg: no jobs are running in the background"))
         }
     } else {
         for arg in args {
             if let Ok(njob) = arg.parse::<usize>() {
-                if !bg_job(shell, njob) {
-                    return FAILURE;
+                let status = bg_job(shell, njob);
+                if !status.is_success() {
+                    return status;
                 }
             } else {
-                eprintln!("ion: bg: {} is not a valid job number", arg);
-                return FAILURE;
+                return Status::error(format!("ion: bg: {} is not a valid job number", arg));
             };
         }
-        SUCCESS
+        Status::SUCCESS
     }
 }
diff --git a/src/lib/builtins/mod.rs b/src/lib/builtins/mod.rs
index 38cde1ccc56c0e010b3c8771cfb518f5b2fca54e..d4e26d975f4d3ce9697c80e597da47006c88e61f 100644
--- a/src/lib/builtins/mod.rs
+++ b/src/lib/builtins/mod.rs
@@ -30,8 +30,7 @@ use self::{
 
 use std::{
     borrow::Cow,
-    error::Error,
-    io::{self, BufRead, Write},
+    io::{self, BufRead},
     path::PathBuf,
 };
 
@@ -39,7 +38,7 @@ use hashbrown::HashMap;
 use liner::{Completer, Context};
 
 use crate::{
-    shell::{status::*, Capture, Shell},
+    shell::{status::Status, Capture, Shell},
     sys, types,
 };
 use itertools::Itertools;
@@ -54,7 +53,7 @@ const DISOWN_DESC: &str =
     "Disowning a process removes that process from the shell's background process table.";
 
 /// The type for builtin functions. Builtins have direct access to the shell
-pub type BuiltinFunction<'a> = &'a dyn Fn(&[small::String], &mut Shell) -> i32;
+pub type BuiltinFunction<'a> = &'a dyn Fn(&[small::String], &mut Shell) -> Status;
 
 macro_rules! map {
     ($builtins:ident, $($name:expr => $func:ident: $help:expr),+) => {{
@@ -81,12 +80,12 @@ fn parse_numeric_arg(arg: &str) -> Option<(bool, usize)> {
 /// Note: To reduce allocations, function are provided as pointer rather than boxed closures
 /// ```
 /// use ion_shell::builtins::BuiltinMap;
-/// use ion_shell::Shell;
+/// use ion_shell::{Shell, status::Status};
 ///
 /// // create a builtin
 /// let mut custom = |_args: &[small::String], _shell: &mut Shell| {
 ///     println!("Hello world!");
-///     42
+///     Status::error("Can't proceed")
 /// };
 ///
 /// // create a builtin map with some predefined builtins
@@ -96,9 +95,8 @@ fn parse_numeric_arg(arg: &str) -> Option<(bool, usize)> {
 /// builtins.add("custom builtin", &mut custom, "Very helpful comment to display to the user");
 ///
 /// // execute a builtin
-/// assert_eq!(
-///     builtins.get("custom builtin").unwrap()(&["ion".into()], &mut Shell::new(false)),
-///     42,
+/// assert!(
+///     builtins.get("custom builtin").unwrap()(&["ion".into()], &mut Shell::new(false)).is_failure(),
 /// );
 /// // >> Hello world!
 pub struct BuiltinMap<'a> {
@@ -121,7 +119,7 @@ impl<'a> Default for BuiltinMap<'a> {
 // If you are implementing a builtin add it to the table below, create a well named manpage in
 // man_pages and check for help flags by adding to the start of your builtin the following
 // if check_help(args, MAN_BUILTIN_NAME) {
-//     return SUCCESS
+//     return Status::SUCCESS
 // }
 impl<'a> BuiltinMap<'a> {
     /// Create a new, blank builtin map
@@ -247,46 +245,36 @@ impl<'a> BuiltinMap<'a> {
     }
 }
 
-fn starts_with(args: &[small::String], _: &mut Shell) -> i32 { conditionals::starts_with(args) }
-fn ends_with(args: &[small::String], _: &mut Shell) -> i32 { conditionals::ends_with(args) }
-fn contains(args: &[small::String], _: &mut Shell) -> i32 { conditionals::contains(args) }
+fn starts_with(args: &[small::String], _: &mut Shell) -> Status {
+    Status::from_exit_code(conditionals::starts_with(args))
+}
+fn ends_with(args: &[small::String], _: &mut Shell) -> Status {
+    Status::from_exit_code(conditionals::ends_with(args))
+}
+fn contains(args: &[small::String], _: &mut Shell) -> Status {
+    Status::from_exit_code(conditionals::contains(args))
+}
 
 // Definitions of simple builtins go here
-pub fn builtin_status(args: &[small::String], shell: &mut Shell) -> i32 {
-    match status(args, shell) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr.write_all(why.as_bytes());
-            FAILURE
-        }
-    }
-}
+pub fn builtin_status(args: &[small::String], shell: &mut Shell) -> Status { status(args, shell) }
 
-pub fn builtin_cd(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_cd(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_CD) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
     match shell.cd(args.get(1)) {
         Ok(()) => {
             let _ = shell.fork_function(Capture::None, |_| Ok(()), "CD_CHANGE", &["ion"]);
-            SUCCESS
-        }
-        Err(why) => {
-            eprintln!("{}", why);
-            FAILURE
+            Status::SUCCESS
         }
+        Err(why) => Status::error(why),
     }
 }
 
-pub fn builtin_bool(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_bool(args: &[small::String], shell: &mut Shell) -> Status {
     if args.len() != 2 {
-        let stderr = io::stderr();
-        let mut stderr = stderr.lock();
-        let _ = stderr.write_all(b"bool requires one argument\n");
-        return FAILURE;
+        return Status::error("bool requires one argument");
     }
 
     let opt = if args[1].is_empty() { None } else { shell.variables().get_str(&args[1][1..]) };
@@ -299,29 +287,21 @@ pub fn builtin_bool(args: &[small::String], shell: &mut Shell) -> i32 {
             "true" => (),
             "--help" => println!("{}", MAN_BOOL),
             "-h" => println!("{}", MAN_BOOL),
-            _ => return FAILURE,
+            _ => return Status::from_exit_code(1),
         },
     }
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_is(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_is(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_IS) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
-    match is(args, shell) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr.write_all(why.as_bytes());
-            FAILURE
-        }
-    }
+    is(args, shell)
 }
 
-pub fn builtin_dirs(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_dirs(args: &[small::String], shell: &mut Shell) -> Status {
     // converts pbuf to an absolute path if possible
     fn try_abs_path(pbuf: &PathBuf) -> Cow<str> {
         Cow::Owned(
@@ -330,7 +310,7 @@ pub fn builtin_dirs(args: &[small::String], shell: &mut Shell) -> i32 {
     }
 
     if check_help(args, MAN_DIRS) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
     let mut clear = false; // -c
@@ -373,30 +353,24 @@ pub fn builtin_dirs(args: &[small::String], shell: &mut Shell) -> i32 {
             Some((false, num)) if shell.dir_stack().count() > num => {
                 shell.dir_stack().count() - num - 1
             }
-            _ => return FAILURE, /* Err(Cow::Owned(format!("ion: dirs: {}: invalid
-                                  * argument\n", arg))) */
+            _ => return Status::error(format!("ion: dirs: {}: invalid argument", arg)),
         };
         match iter.nth(num) {
             Some(x) => {
                 println!("{}", x);
-                SUCCESS
+                Status::SUCCESS
             }
-            None => FAILURE,
+            None => Status::error(""),
         }
     } else {
-        let folder: fn(String, Cow<str>) -> String =
-            if multiline { |x, y| x + "\n" + &y } else { |x, y| x + " " + &y };
-
-        if let Some(x) = iter.next() {
-            println!("{}", iter.fold(x.to_string(), folder));
-        }
-        SUCCESS
+        println!("{}", iter.join(if multiline { "\n" } else { " " }));
+        Status::SUCCESS
     }
 }
 
-pub fn builtin_pushd(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_pushd(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_PUSHD) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
     enum Action {
@@ -421,8 +395,7 @@ pub fn builtin_pushd(args: &[small::String], shell: &mut Shell) -> i32 {
                 None => Action::Push(PathBuf::from(arg)), // no numeric arg => `dir`-parameter
             };
         } else {
-            eprintln!("ion: pushd: too many arguments");
-            return FAILURE;
+            return Status::error("ion: pushd: too many arguments");
         }
     }
 
@@ -430,31 +403,27 @@ pub fn builtin_pushd(args: &[small::String], shell: &mut Shell) -> i32 {
         Action::Switch => {
             if !keep_front {
                 if let Err(why) = shell.swap(1) {
-                    eprintln!("ion: pushd: {}", why);
-                    return FAILURE;
+                    return Status::error(format!("ion: pushd: {}", why));
                 }
             }
         }
         Action::RotLeft(num) => {
             if !keep_front {
                 if let Err(why) = shell.rotate_left(num) {
-                    eprintln!("ion: pushd: {}", why);
-                    return FAILURE;
+                    return Status::error(format!("ion: pushd: {}", why));
                 }
             }
         }
         Action::RotRight(num) => {
             if !keep_front {
                 if let Err(why) = shell.rotate_right(num) {
-                    eprintln!("ion: pushd: {}", why);
-                    return FAILURE;
+                    return Status::error(format!("ion: pushd: {}", why));
                 }
             }
         }
         Action::Push(dir) => {
             if let Err(why) = shell.pushd(dir, keep_front) {
-                eprintln!("ion: pushd: {}", why);
-                return FAILURE;
+                return Status::error(format!("ion: pushd: {}", why));
             }
         }
     };
@@ -463,18 +432,17 @@ pub fn builtin_pushd(args: &[small::String], shell: &mut Shell) -> i32 {
         "{}",
         shell.dir_stack().map(|dir| dir.to_str().unwrap_or("ion: no directory found")).join(" ")
     );
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_popd(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_popd(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_POPD) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
     let len = shell.dir_stack().len();
     if len <= 1 {
-        eprintln!("ion: popd: directory stack empty");
-        return FAILURE;
+        return Status::error("ion: popd: directory stack empty");
     }
 
     let mut keep_front = false; // whether the -n option is present
@@ -488,8 +456,7 @@ pub fn builtin_popd(args: &[small::String], shell: &mut Shell) -> i32 {
             let (count_from_front, num) = match parse_numeric_arg(arg) {
                 Some(n) => n,
                 None => {
-                    eprintln!("ion: popd: {}: invalid argument", arg);
-                    return FAILURE;
+                    return Status::error(format!("ion: popd: {}: invalid argument", arg));
                 }
             };
 
@@ -499,8 +466,7 @@ pub fn builtin_popd(args: &[small::String], shell: &mut Shell) -> i32 {
             } else if let Some(n) = (len - 1).checked_sub(num) {
                 n
             } else {
-                eprintln!("ion: popd: negative directory stack index out of range");
-                return FAILURE;
+                return Status::error("ion: popd: negative directory stack index out of range");
             };
         }
     }
@@ -511,8 +477,7 @@ pub fn builtin_popd(args: &[small::String], shell: &mut Shell) -> i32 {
     } else if index == 0 {
         // change to new directory, return if not possible
         if let Err(why) = shell.set_current_dir_by_index(1) {
-            eprintln!("ion: popd: {}", why);
-            return FAILURE;
+            return Status::error(format!("ion: popd: {}", why));
         }
     }
 
@@ -525,25 +490,24 @@ pub fn builtin_popd(args: &[small::String], shell: &mut Shell) -> i32 {
                 .map(|dir| dir.to_str().unwrap_or("ion: no directory found"))
                 .join(" ")
         );
-        SUCCESS
+        Status::SUCCESS
     } else {
-        eprintln!("ion: popd: {}: directory stack index out of range", index);
-        FAILURE
+        Status::error(format!("ion: popd: {}: directory stack index out of range", index))
     }
 }
 
-pub fn builtin_alias(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_alias(args: &[small::String], shell: &mut Shell) -> Status {
     let args_str = args[1..].join(" ");
     alias(shell.variables_mut(), &args_str)
 }
 
-pub fn builtin_unalias(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_unalias(args: &[small::String], shell: &mut Shell) -> Status {
     drop_alias(shell.variables_mut(), args)
 }
 
 // TODO There is a man page for fn however the -h and --help flags are not
 // checked for.
-pub fn builtin_fn(_: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_fn(_: &[small::String], shell: &mut Shell) -> Status {
     print_functions(shell.variables())
 }
 
@@ -553,9 +517,9 @@ impl Completer for EmptyCompleter {
     fn completions(&mut self, _start: &str) -> Vec<String> { Vec::new() }
 }
 
-pub fn builtin_read(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_read(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_READ) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
     if sys::isatty(sys::STDIN_FILENO) {
@@ -565,7 +529,7 @@ pub fn builtin_read(args: &[small::String], shell: &mut Shell) -> i32 {
                 Ok(buffer) => {
                     shell.variables_mut().set(arg.as_ref(), buffer.trim());
                 }
-                Err(_) => return FAILURE,
+                Err(_) => return Status::error(""),
             }
         }
     } else {
@@ -578,12 +542,12 @@ pub fn builtin_read(args: &[small::String], shell: &mut Shell) -> i32 {
             }
         }
     }
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_drop(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_drop(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_DROP) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     if args.len() >= 2 && args[1] == "-a" {
         drop_array(shell.variables_mut(), args)
@@ -592,194 +556,154 @@ pub fn builtin_drop(args: &[small::String], shell: &mut Shell) -> i32 {
     }
 }
 
-pub fn builtin_set(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_set(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_SET) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     set::set(args, shell)
 }
 
-pub fn builtin_eq(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_eq(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_EQ) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
-    match is(args, shell) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            eprintln!("{}", why);
-            FAILURE
-        }
-    }
+    is(args, shell)
 }
 
-pub fn builtin_eval(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_eval(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_EVAL) {
-        SUCCESS
+        Status::SUCCESS
     } else {
         shell.execute_command(args[1..].join(" ").as_bytes()).unwrap_or_else(|_| {
-            eprintln!("ion: supplied eval expression was not terminated");
-            FAILURE
+            Status::error(format!("ion: supplied eval expression was not terminated"))
         })
     }
 }
 
-pub fn builtin_source(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_source(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_SOURCE) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     match source(shell, args) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr.write_all(why.as_bytes());
-            FAILURE
-        }
+        Ok(()) => Status::SUCCESS,
+        Err(why) => Status::error(why),
     }
 }
 
-pub fn builtin_echo(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_echo(args: &[small::String], _: &mut Shell) -> Status {
     if check_help(args, MAN_ECHO) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     match echo(args) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr.write_all(why.description().as_bytes());
-            FAILURE
-        }
+        Ok(()) => Status::SUCCESS,
+        Err(why) => Status::error(why.to_string()),
     }
 }
 
-pub fn builtin_test(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_test(args: &[small::String], _: &mut Shell) -> Status {
     // Do not use `check_help` for the `test` builtin. The
     // `test` builtin contains a "-h" option.
     match test(args) {
-        Ok(true) => SUCCESS,
-        Ok(false) => FAILURE,
-        Err(why) => {
-            eprintln!("{}", why);
-            FAILURE
-        }
+        Ok(true) => Status::SUCCESS,
+        Ok(false) => Status::error(""),
+        Err(why) => Status::error(why),
     }
 }
 
 // TODO create manpage.
-pub fn builtin_calc(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_calc(args: &[small::String], _: &mut Shell) -> Status {
     match calc::calc(&args[1..]) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            eprintln!("{}", why);
-            FAILURE
-        }
+        Ok(()) => Status::SUCCESS,
+        Err(why) => Status::error(why),
     }
 }
 
-pub fn builtin_random(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_random(args: &[small::String], _: &mut Shell) -> Status {
     if check_help(args, MAN_RANDOM) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     match random::random(&args[1..]) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            eprintln!("{}", why);
-            FAILURE
-        }
+        Ok(()) => Status::SUCCESS,
+        Err(why) => Status::error(why),
     }
 }
 
-pub fn builtin_true(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_true(args: &[small::String], _: &mut Shell) -> Status {
     check_help(args, MAN_TRUE);
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_false(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_false(args: &[small::String], _: &mut Shell) -> Status {
     if check_help(args, MAN_FALSE) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
-    FAILURE
+    Status::error("")
 }
 
 // TODO create a manpage
-pub fn builtin_wait(_: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_wait(_: &[small::String], shell: &mut Shell) -> Status {
     shell.wait_for_background();
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_jobs(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_jobs(args: &[small::String], shell: &mut Shell) -> Status {
     check_help(args, MAN_JOBS);
     job_control::jobs(shell);
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_bg(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_bg(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_BG) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     job_control::bg(shell, &args[1..])
 }
 
-pub fn builtin_fg(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_fg(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_FG) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     job_control::fg(shell, &args[1..])
 }
 
-pub fn builtin_suspend(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_suspend(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_SUSPEND) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     shell.suspend();
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_disown(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_disown(args: &[small::String], shell: &mut Shell) -> Status {
     for arg in args {
         if *arg == "--help" {
             println!("{}", MAN_DISOWN);
-            return SUCCESS;
+            return Status::SUCCESS;
         }
     }
     match job_control::disown(shell, &args[1..]) {
-        Ok(()) => SUCCESS,
-        Err(err) => {
-            eprintln!("ion: disown: {}", err);
-            FAILURE
-        }
+        Ok(()) => Status::SUCCESS,
+        Err(err) => Status::error(format!("ion: disown: {}", err)),
     }
 }
 
-pub fn builtin_help(args: &[small::String], shell: &mut Shell) -> i32 {
-    let builtins = shell.builtins();
-    let stdout = io::stdout();
-    let mut stdout = stdout.lock();
+pub fn builtin_help(args: &[small::String], shell: &mut Shell) -> Status {
     if let Some(command) = args.get(1) {
-        if let Some(help) = builtins.get_help(command) {
-            let _ = stdout.write_all(help.as_bytes());
-            let _ = stdout.write_all(b"\n");
+        if let Some(help) = shell.builtins().get_help(command) {
+            println!("{}", help);
         } else {
-            let _ = stdout.write_all(b"Command helper not found [run 'help']...");
-            let _ = stdout.write_all(b"\n");
+            println!("Command helper not found [run 'help']...");
         }
     } else {
-        let commands = builtins.keys();
-
-        let mut buffer: Vec<u8> = Vec::new();
-        for command in commands {
-            let _ = writeln!(buffer, "{}", command);
-        }
-        let _ = stdout.write_all(&buffer);
+        println!("{}", shell.builtins().keys().join(""));
     }
-    SUCCESS
+    Status::SUCCESS
 }
 
-pub fn builtin_exit(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_exit(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_EXIT) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     // Kill all active background tasks before exiting the shell.
     for process in shell.background_jobs().iter() {
@@ -787,109 +711,93 @@ pub fn builtin_exit(args: &[small::String], shell: &mut Shell) -> i32 {
             let _ = sys::kill(process.pid(), sys::SIGTERM);
         }
     }
-    shell.exit(args.get(1).and_then(|status| status.parse::<i32>().ok()))
+    if let Some(status) = args.get(1).and_then(|status| status.parse::<i32>().ok()) {
+        shell.exit_with_code(Status::from_exit_code(status))
+    } else {
+        shell.exit()
+    }
 }
 
-pub fn builtin_exec(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_exec(args: &[small::String], shell: &mut Shell) -> Status {
     match exec(shell, &args[1..]) {
         // Shouldn't ever hit this case.
-        Ok(()) => SUCCESS,
-        Err(err) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = writeln!(stderr, "ion: exec: {}", err);
-            FAILURE
-        }
+        Ok(()) => Status::SUCCESS,
+        Err(err) => Status::error(format!("ion: exec: {}", err)),
     }
 }
 
 use regex::Regex;
-pub fn builtin_matches(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_matches(args: &[small::String], _: &mut Shell) -> Status {
     if check_help(args, MAN_MATCHES) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     if args[1..].len() != 2 {
-        let stderr = io::stderr();
-        let mut stderr = stderr.lock();
-        let _ = stderr.write_all(b"match takes two arguments\n");
-        return BAD_ARG;
+        eprintln!("match takes two arguments");
+        return Status::BAD_ARG;
     }
     let input = &args[1];
     let re = match Regex::new(&args[2]) {
         Ok(r) => r,
         Err(e) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr
-                .write_all(format!("couldn't compile input regex {}: {}\n", args[2], e).as_bytes());
-            return FAILURE;
+            return Status::error(format!("couldn't compile input regex {}: {}", args[2], e));
         }
     };
 
     if re.is_match(input) {
-        SUCCESS
+        Status::SUCCESS
     } else {
-        FAILURE
+        Status::error("")
     }
 }
 
-pub fn builtin_exists(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_exists(args: &[small::String], shell: &mut Shell) -> Status {
     if check_help(args, MAN_EXISTS) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
     match exists(args, shell) {
-        Ok(true) => SUCCESS,
-        Ok(false) => FAILURE,
-        Err(why) => {
-            eprintln!("{}", why);
-            FAILURE
-        }
+        Ok(true) => Status::SUCCESS,
+        Ok(false) => Status::error(""),
+        Err(why) => Status::error(why),
     }
 }
 
-pub fn builtin_which(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_which(args: &[small::String], shell: &mut Shell) -> Status {
     match which(args, shell) {
         Ok(result) => result,
-        Err(()) => FAILURE,
+        Err(()) => Status::error(""),
     }
 }
 
-pub fn builtin_type(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn builtin_type(args: &[small::String], shell: &mut Shell) -> Status {
     match find_type(args, shell) {
         Ok(result) => result,
-        Err(()) => FAILURE,
+        Err(()) => Status::error(""),
     }
 }
 
-pub fn builtin_isatty(args: &[small::String], _: &mut Shell) -> i32 {
+pub fn builtin_isatty(args: &[small::String], _: &mut Shell) -> Status {
     if check_help(args, MAN_ISATTY) {
-        return SUCCESS;
+        return Status::SUCCESS;
     }
 
     if args.len() > 1 {
         // sys::isatty expects a usize if compiled for redox but otherwise a i32.
         #[cfg(target_os = "redox")]
-        match args[1].parse::<usize>() {
-            Ok(r) => {
-                if sys::isatty(r) {
-                    return SUCCESS;
-                }
-            }
-            Err(_) => eprintln!("ion: isatty given bad number"),
-        }
-
+        let pid = args[1].parse::<usize>();
         #[cfg(not(target_os = "redox"))]
-        match args[1].parse::<i32>() {
+        let pid = args[1].parse::<i32>();
+
+        match pid {
             Ok(r) => {
                 if sys::isatty(r) {
-                    return SUCCESS;
+                    Status::SUCCESS
+                } else {
+                    Status::error("")
                 }
             }
-            Err(_) => eprintln!("ion: isatty given bad number"),
+            Err(_) => Status::error("ion: isatty given bad number"),
         }
     } else {
-        return SUCCESS;
+        Status::SUCCESS
     }
-
-    FAILURE
 }
diff --git a/src/lib/builtins/set.rs b/src/lib/builtins/set.rs
index 15115563147ec34e3b8b6b9cf5d5e500ce6f724e..de2095a6c0475ffad47208f9dae14e0a1f433e5f 100644
--- a/src/lib/builtins/set.rs
+++ b/src/lib/builtins/set.rs
@@ -1,5 +1,5 @@
 use crate::{
-    shell::{variables::Value, Shell},
+    shell::{status::Status, variables::Value, Shell},
     types,
 };
 use small;
@@ -12,7 +12,7 @@ enum PositionalArgs {
 
 use self::PositionalArgs::*;
 
-pub fn set(args: &[small::String], shell: &mut Shell) -> i32 {
+pub fn set(args: &[small::String], shell: &mut Shell) -> Status {
     let mut args_iter = args.iter();
     let mut positionals = None;
 
@@ -22,7 +22,7 @@ pub fn set(args: &[small::String], shell: &mut Shell) -> i32 {
                 positionals = Some(UnsetIfNone);
                 break;
             }
-            return 0;
+            return Status::SUCCESS;
         } else if arg.starts_with('-') {
             if arg.len() == 1 {
                 positionals = Some(RetainIfNone);
@@ -31,7 +31,7 @@ pub fn set(args: &[small::String], shell: &mut Shell) -> i32 {
             for flag in arg.bytes().skip(1) {
                 match flag {
                     b'e' => shell.opts_mut().err_exit = true,
-                    _ => return 0,
+                    _ => return Status::SUCCESS,
                 }
             }
         } else if arg.starts_with('+') {
@@ -42,15 +42,13 @@ pub fn set(args: &[small::String], shell: &mut Shell) -> i32 {
                     b'o' => match args_iter.next().map(|s| s as &str) {
                         Some("huponexit") => shell.opts_mut().huponexit = false,
                         Some(_) => {
-                            eprintln!("ion: set: invalid option");
-                            return 0;
+                            return Status::error(format!("ion: set: invalid option"));
                         }
                         None => {
-                            eprintln!("ion: set: no option given");
-                            return 0;
+                            return Status::error(format!("ion: set: no option given"));
                         }
                     },
-                    _ => return 0,
+                    _ => return Status::SUCCESS,
                 }
             }
         }
@@ -80,5 +78,5 @@ pub fn set(args: &[small::String], shell: &mut Shell) -> i32 {
         }
     }
 
-    0
+    Status::SUCCESS
 }
diff --git a/src/lib/builtins/status.rs b/src/lib/builtins/status.rs
index baa6fcdf0ca64bc2c34d4e5409fb2bef1edf969f..ca6fdbbc89f69626c6649a9f55e41917c3b65dc7 100644
--- a/src/lib/builtins/status.rs
+++ b/src/lib/builtins/status.rs
@@ -1,8 +1,11 @@
-use crate::{builtins::man_pages::MAN_STATUS, shell::Shell};
+use crate::{
+    builtins::man_pages::MAN_STATUS,
+    shell::{status::Status, Shell},
+};
 use small;
 use std::env;
 
-pub fn status(args: &[small::String], shell: &mut Shell) -> Result<(), String> {
+pub fn status(args: &[small::String], shell: &mut Shell) -> Status {
     let mut help = false;
     let mut login_shell = false;
     let mut interactive = false;
@@ -33,11 +36,11 @@ pub fn status(args: &[small::String], shell: &mut Shell) -> Result<(), String> {
             }
 
             if login_shell && !is_login {
-                return Err("".to_string());
+                return Status::error("");
             }
 
             if interactive && shell.opts().is_background_shell {
-                return Err("".to_string());
+                return Status::error("");
             }
 
             if filename {
@@ -55,7 +58,7 @@ pub fn status(args: &[small::String], shell: &mut Shell) -> Result<(), String> {
                 println!("{}", MAN_STATUS);
             }
 
-            Ok(())
+            Status::SUCCESS
         }
         1 => {
             if is_login {
@@ -63,8 +66,8 @@ pub fn status(args: &[small::String], shell: &mut Shell) -> Result<(), String> {
             } else {
                 println!("This is not a login shell");
             }
-            Ok(())
+            Status::SUCCESS
         }
-        _ => Err("status takes one argument\n".to_string()),
+        _ => Status::error("status takes one argument"),
     }
 }
diff --git a/src/lib/builtins/variables.rs b/src/lib/builtins/variables.rs
index d03c45b66dbb46e621b0b4cb8c2361f73bda74f7..bef81a0d270eddac0840aa1e3f0e71435bede848 100644
--- a/src/lib/builtins/variables.rs
+++ b/src/lib/builtins/variables.rs
@@ -3,7 +3,7 @@
 use std::io::{self, Write};
 
 use crate::{
-    shell::{status::*, variables::Variables},
+    shell::{status::Status, variables::Variables},
     types,
 };
 
@@ -12,11 +12,7 @@ fn print_list(vars: &Variables) {
     let stdout = &mut stdout.lock();
 
     for (key, value) in vars.aliases() {
-        let _ = stdout
-            .write(key.as_bytes())
-            .and_then(|_| stdout.write_all(b" = "))
-            .and_then(|_| stdout.write_all(value.as_bytes()))
-            .and_then(|_| stdout.write_all(b"\n"));
+        writeln!(stdout, "{} = {}", key, value).unwrap();
     }
 }
 
@@ -69,84 +65,72 @@ fn parse_alias(args: &str) -> Binding {
 
 /// The `alias` command will define an alias for another command, and thus may be used as a
 /// command itself.
-pub fn alias(vars: &mut Variables, args: &str) -> i32 {
+pub fn alias(vars: &mut Variables, args: &str) -> Status {
     match parse_alias(args) {
         Binding::InvalidKey(key) => {
-            eprintln!("ion: alias name, '{}', is invalid", key);
-            return FAILURE;
+            return Status::error(format!("ion: alias name, '{}', is invalid", key));
         }
         Binding::KeyValue(key, value) => {
             vars.set(&key, types::Alias(value));
         }
         Binding::ListEntries => print_list(&vars),
         Binding::KeyOnly(key) => {
-            eprintln!("ion: please provide value for alias '{}'", key);
-            return FAILURE;
+            return Status::error(format!("ion: please provide value for alias '{}'", key));
         }
     }
-    SUCCESS
+    Status::SUCCESS
 }
 
 /// Dropping an alias will erase it from the shell.
-pub fn drop_alias<S: AsRef<str>>(vars: &mut Variables, args: &[S]) -> i32 {
+pub fn drop_alias<S: AsRef<str>>(vars: &mut Variables, args: &[S]) -> Status {
     if args.len() <= 1 {
-        eprintln!("ion: you must specify an alias name");
-        return FAILURE;
+        return Status::error(format!("ion: you must specify an alias name"));
     }
     for alias in args.iter().skip(1) {
         if vars.remove_variable(alias.as_ref()).is_none() {
-            eprintln!("ion: undefined alias: {}", alias.as_ref());
-            return FAILURE;
+            return Status::error(format!("ion: undefined alias: {}", alias.as_ref()));
         }
     }
-    SUCCESS
+    Status::SUCCESS
 }
 
 /// Dropping an array will erase it from the shell.
-pub fn drop_array<S: AsRef<str>>(vars: &mut Variables, args: &[S]) -> i32 {
+pub fn drop_array<S: AsRef<str>>(vars: &mut Variables, args: &[S]) -> Status {
     if args.len() <= 2 {
-        eprintln!("ion: you must specify an array name");
-        return FAILURE;
+        return Status::error(format!("ion: you must specify an array name"));
     }
 
     if args[1].as_ref() != "-a" {
-        eprintln!("ion: drop_array must be used with -a option");
-        return FAILURE;
+        return Status::error(format!("ion: drop_array must be used with -a option"));
     }
 
     for array in args.iter().skip(2) {
         if vars.remove_variable(array.as_ref()).is_none() {
-            eprintln!("ion: undefined array: {}", array.as_ref());
-            return FAILURE;
+            return Status::error(format!("ion: undefined array: {}", array.as_ref()));
         }
     }
-    SUCCESS
+    Status::SUCCESS
 }
 
 /// Dropping a variable will erase it from the shell.
-pub fn drop_variable<S: AsRef<str>>(vars: &mut Variables, args: &[S]) -> i32 {
+pub fn drop_variable<S: AsRef<str>>(vars: &mut Variables, args: &[S]) -> Status {
     if args.len() <= 1 {
-        eprintln!("ion: you must specify a variable name");
-        return FAILURE;
+        return Status::error(format!("ion: you must specify a variable name"));
     }
 
     for variable in args.iter().skip(1) {
         if vars.remove_variable(variable.as_ref()).is_none() {
-            eprintln!("ion: undefined variable: {}", variable.as_ref());
-            return FAILURE;
+            return Status::error(format!("ion: undefined variable: {}", variable.as_ref()));
         }
     }
 
-    SUCCESS
+    Status::SUCCESS
 }
 
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::{
-        parser::Expander,
-        shell::status::{FAILURE, SUCCESS},
-    };
+    use crate::parser::Expander;
 
     struct VariableExpander<'a>(pub Variables<'a>);
 
@@ -183,7 +167,7 @@ mod test {
         let mut variables = Variables::default();
         variables.set("FOO", "BAR");
         let return_status = drop_variable(&mut variables, &["drop", "FOO"]);
-        assert_eq!(SUCCESS, return_status);
+        assert!(return_status.is_success());
         let expanded = VariableExpander(variables).expand_string("$FOO").join("");
         assert_eq!("", expanded);
     }
@@ -192,14 +176,14 @@ mod test {
     fn drop_fails_with_no_arguments() {
         let mut variables = Variables::default();
         let return_status = drop_variable(&mut variables, &["drop"]);
-        assert_eq!(FAILURE, return_status);
+        assert!(!return_status.is_success());
     }
 
     #[test]
     fn drop_fails_with_undefined_variable() {
         let mut variables = Variables::default();
         let return_status = drop_variable(&mut variables, &["drop", "FOO"]);
-        assert_eq!(FAILURE, return_status);
+        assert!(!return_status.is_success());
     }
 
     #[test]
@@ -207,7 +191,7 @@ mod test {
         let mut variables = Variables::default();
         variables.set("FOO", array!["BAR"]);
         let return_status = drop_array(&mut variables, &["drop", "-a", "FOO"]);
-        assert_eq!(SUCCESS, return_status);
+        assert_eq!(Status::SUCCESS, return_status);
         let expanded = VariableExpander(variables).expand_string("@FOO").join("");
         assert_eq!("", expanded);
     }
@@ -216,13 +200,13 @@ mod test {
     fn drop_array_fails_with_no_arguments() {
         let mut variables = Variables::default();
         let return_status = drop_array(&mut variables, &["drop", "-a"]);
-        assert_eq!(FAILURE, return_status);
+        assert!(!return_status.is_success());
     }
 
     #[test]
     fn drop_array_fails_with_undefined_array() {
         let mut variables = Variables::default();
         let return_status = drop_array(&mut variables, &["drop", "FOO"]);
-        assert_eq!(FAILURE, return_status);
+        assert!(!return_status.is_success());
     }
 }
diff --git a/src/lib/parser/statement/mod.rs b/src/lib/parser/statement/mod.rs
index 6f0fe6b2d7d58371db4c260a2ad1dd0ff7fda1a6..1863302b2497d9d8e237b019b25a2549235527ea 100644
--- a/src/lib/parser/statement/mod.rs
+++ b/src/lib/parser/statement/mod.rs
@@ -7,7 +7,10 @@ pub use self::{
     parse::{is_valid_name, parse},
     splitter::{StatementError, StatementSplitter, StatementVariant},
 };
-use crate::{builtins::BuiltinMap, shell::flow_control::Statement};
+use crate::{
+    builtins::BuiltinMap,
+    shell::{flow_control::Statement, status::Status},
+};
 
 /// Parses a given statement string and return's the corresponding mapped
 /// `Statement`
@@ -23,7 +26,7 @@ pub fn parse_and_validate<'b>(
         Ok(StatementVariant::Default(statement)) => parse(statement, builtins),
         Err(err) => {
             eprintln!("ion: {}", err);
-            Statement::Error(-1)
+            Statement::Error(Status::from_exit_code(-1))
         }
     }
 }
diff --git a/src/lib/parser/statement/parse.rs b/src/lib/parser/statement/parse.rs
index 17379be1c42ecdf026208bfea87cfdb953587f5a..f53642cf5213ca5df889d5bf2df49b40d034c3ff 100644
--- a/src/lib/parser/statement/parse.rs
+++ b/src/lib/parser/statement/parse.rs
@@ -8,7 +8,7 @@ use crate::{
     lexers::{assignment_lexer, ArgumentSplitter},
     shell::{
         flow_control::{Case, ElseIf, ExportAction, IfMode, LocalAction, Statement},
-        status::FAILURE,
+        status::Status,
     },
 };
 use small;
@@ -27,8 +27,7 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
         "break" => Statement::Break,
         "continue" => Statement::Continue,
         "for" | "match" | "case" => {
-            eprintln!("ion: syntax error: incomplete control flow statement");
-            Statement::Error(FAILURE)
+            Statement::Error(Status::error("ion: syntax error: incomplete control flow statement"))
         }
         "let" => Statement::Let(LocalAction::List),
         _ if cmd.starts_with("let ") => {
@@ -45,11 +44,14 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
                 }
                 None => {
                     if op.is_none() {
-                        eprintln!("ion: assignment error: no operator supplied.");
+                        Statement::Error(Status::error(
+                            "ion: assignment error: no operator supplied.",
+                        ))
                     } else {
-                        eprintln!("ion: assignment error: no values supplied.");
+                        Statement::Error(Status::error(
+                            "ion: assignment error: no values supplied.",
+                        ))
                     }
-                    Statement::Error(FAILURE)
                 }
             }
         }
@@ -68,13 +70,14 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
                 }
                 None => {
                     if keys.is_none() {
-                        eprintln!("ion: assignment error: no keys supplied.")
+                        Statement::Error(Status::error("ion: assignment error: no keys supplied."))
                     } else if op.is_some() {
-                        eprintln!("ion: assignment error: no values supplied.")
+                        Statement::Error(Status::error(
+                            "ion: assignment error: no values supplied.",
+                        ))
                     } else {
-                        return Statement::Export(ExportAction::LocalExport(keys.unwrap().into()));
+                        Statement::Export(ExportAction::LocalExport(keys.unwrap().into()))
                     }
-                    Statement::Error(FAILURE)
                 }
             }
         }
@@ -132,10 +135,9 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
                     values: ArgumentSplitter::new(cmd).map(small::String::from).collect(),
                     statements: Vec::new(),
                 },
-                None => {
-                    eprintln!("ion: syntax error: for loop lacks the `in` keyword");
-                    Statement::Error(FAILURE)
-                }
+                None => Statement::Error(Status::error(
+                    "ion: syntax error: for loop lacks the `in` keyword",
+                )),
             }
         }
         _ if cmd.starts_with("case ") => {
@@ -145,8 +147,10 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
                     let (value, binding, conditional) = match case::parse_case(value) {
                         Ok(values) => values,
                         Err(why) => {
-                            eprintln!("ion: case error: {}", why);
-                            return Statement::Error(FAILURE);
+                            return Statement::Error(Status::error(format!(
+                                "ion: case error: {}",
+                                why
+                            )))
                         }
                     };
                     let binding = binding.map(Into::into);
@@ -168,12 +172,11 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
             let pos = cmd.find(char::is_whitespace).unwrap_or_else(|| cmd.len());
             let name = &cmd[..pos];
             if !is_valid_name(name) {
-                eprintln!(
+                return Statement::Error(Status::error(format!(
                     "ion: syntax error: '{}' is not a valid function name\n     Function names \
                      may only contain alphanumeric characters",
                     name
-                );
-                return Statement::Error(FAILURE);
+                )));
             }
 
             let (args, description) = parse_function(&cmd[pos..]);
@@ -184,10 +187,10 @@ pub fn parse<'a>(code: &str, builtins: &BuiltinMap<'a>) -> Statement<'a> {
                     args,
                     statements: Vec::new(),
                 },
-                Err(why) => {
-                    eprintln!("ion: function argument error: {}", why);
-                    Statement::Error(FAILURE)
-                }
+                Err(why) => Statement::Error(Status::error(format!(
+                    "ion: function argument error: {}",
+                    why
+                ))),
             }
         }
         _ if cmd.starts_with("time ") => {
diff --git a/src/lib/shell/assignments.rs b/src/lib/shell/assignments.rs
index cb9b9fcca20e4115ebfedfa1a7bca50468588489..f762e1f5a57a8e02b868040242380151022729e5 100644
--- a/src/lib/shell/assignments.rs
+++ b/src/lib/shell/assignments.rs
@@ -42,7 +42,7 @@ fn list_vars(shell: &Shell) -> Result<(), io::Error> {
 /// exporting variables to some global environment
 impl<'b> Shell<'b> {
     /// Export a variable to the process environment given a binding
-    pub fn export(&mut self, action: &ExportAction) -> i32 {
+    pub fn export(&mut self, action: &ExportAction) -> Status {
         match action {
             ExportAction::Assign(ref keys, op, ref vals) => {
                 let actions = AssignmentActions::new(keys, *op, vals);
@@ -73,21 +73,19 @@ impl<'b> Shell<'b> {
                     });
 
                     if let Err(why) = err {
-                        eprintln!("ion: assignment error: {}", why);
-                        return FAILURE;
+                        return Status::error(format!("ion: assignment error: {}", why));
                     }
                 }
 
-                SUCCESS
+                Status::SUCCESS
             }
             ExportAction::LocalExport(ref key) => match self.variables.get_str(key) {
                 Some(var) => {
                     env::set_var(key, &*var);
-                    SUCCESS
+                    Status::SUCCESS
                 }
                 None => {
-                    eprintln!("ion: cannot export {} because it does not exist.", key);
-                    FAILURE
+                    Status::error(format!("ion: cannot export {} because it does not exist.", key))
                 }
             },
             ExportAction::List => {
@@ -96,7 +94,7 @@ impl<'b> Shell<'b> {
                 for (key, val) in env::vars() {
                     let _ = writeln!(stdout, "{} = \"{}\"", key, val);
                 }
-                SUCCESS
+                Status::SUCCESS
             }
         }
     }
@@ -159,11 +157,11 @@ impl<'b> Shell<'b> {
     }
 
     /// Set a local variable given a binding
-    pub fn local(&mut self, action: &LocalAction) -> i32 {
+    pub fn local(&mut self, action: &LocalAction) -> Status {
         match action {
             LocalAction::List => {
                 let _ = list_vars(&self);
-                SUCCESS
+                Status::SUCCESS
             }
             LocalAction::Assign(ref keys, op, ref vals) => {
                 let actions = AssignmentActions::new(keys, *op, vals);
@@ -173,10 +171,9 @@ impl<'b> Shell<'b> {
                     }
                     Ok(())
                 }) {
-                    eprintln!("ion: assignment error: {}", why);
-                    FAILURE
+                    Status::error(format!("ion: assignment error: {}", why))
                 } else {
-                    SUCCESS
+                    Status::SUCCESS
                 }
             }
         }
diff --git a/src/lib/shell/flow.rs b/src/lib/shell/flow.rs
index 131c0d346c513c036d05717eb5a23b572106b5c5..dced4229476aeab90c00603ddb13e0cab0472c07 100644
--- a/src/lib/shell/flow.rs
+++ b/src/lib/shell/flow.rs
@@ -39,7 +39,7 @@ impl<'a> Shell<'a> {
         if let Condition::SigInt = self.execute_statements(&expression) {
             return Condition::SigInt;
         }
-        if self.previous_status == 0 {
+        if self.previous_status.is_success() {
             return self.execute_statements(&success);
         }
 
@@ -49,7 +49,7 @@ impl<'a> Shell<'a> {
                 return Condition::SigInt;
             }
 
-            if self.previous_status == 0 {
+            if self.previous_status.is_success() {
                 return self.execute_statements(&success);
             }
         }
@@ -118,7 +118,7 @@ impl<'a> Shell<'a> {
     ) -> Condition {
         loop {
             self.execute_statements(expression);
-            if self.previous_status != 0 {
+            if !self.previous_status.is_success() {
                 return Condition::NoOp;
             }
 
@@ -136,16 +136,16 @@ impl<'a> Shell<'a> {
         match statement {
             Statement::Error(number) => {
                 self.previous_status = *number;
-                self.variables.set("?", self.previous_status.to_string());
+                self.variables.set("?", self.previous_status);
                 self.flow_control.clear();
             }
             Statement::Let(action) => {
                 self.previous_status = self.local(action);
-                self.variables.set("?", self.previous_status.to_string());
+                self.variables.set("?", self.previous_status);
             }
             Statement::Export(action) => {
                 self.previous_status = self.export(action);
-                self.variables.set("?", self.previous_status.to_string());
+                self.variables.set("?", self.previous_status);
             }
             Statement::While { expression, statements } => {
                 if self.execute_while(&expression, &statements) == Condition::SigInt {
@@ -180,17 +180,17 @@ impl<'a> Shell<'a> {
                     if !pipeline.items.is_empty() {
                         self.run_pipeline(pipeline);
                     }
-                    if self.opts.err_exit && self.previous_status != SUCCESS {
-                        self.exit(None);
+                    if self.opts.err_exit && !self.previous_status.is_success() {
+                        self.exit();
                     }
                     if !statements.is_empty() {
                         self.execute_statements(&statements);
                     }
                 }
                 Err(e) => {
-                    eprintln!("ion: pipeline expansion error: {}", e);
-                    self.previous_status = FAILURE;
-                    self.variables.set("?", self.previous_status.to_string());
+                    self.previous_status =
+                        Status::error(format!("ion: pipeline expansion error: {}", e));
+                    self.variables.set("?", self.previous_status);
                     self.flow_control.clear();
                     return Condition::Break;
                 }
@@ -214,9 +214,10 @@ impl<'a> Shell<'a> {
                 }
             }
             Statement::And(box_statement) => {
-                let condition = match self.previous_status {
-                    SUCCESS => self.execute_statement(box_statement),
-                    _ => Condition::NoOp,
+                let condition = if self.previous_status.is_success() {
+                    self.execute_statement(box_statement)
+                } else {
+                    Condition::NoOp
                 };
 
                 if condition != Condition::NoOp {
@@ -224,9 +225,10 @@ impl<'a> Shell<'a> {
                 }
             }
             Statement::Or(box_statement) => {
-                let condition = match self.previous_status {
-                    FAILURE => self.execute_statement(box_statement),
-                    _ => Condition::NoOp,
+                let condition = if self.previous_status.is_success() {
+                    Condition::NoOp
+                } else {
+                    self.execute_statement(box_statement)
                 };
 
                 if condition != Condition::NoOp {
@@ -236,13 +238,8 @@ impl<'a> Shell<'a> {
             Statement::Not(box_statement) => {
                 // NOTE: Should the condition be used?
                 let _condition = self.execute_statement(box_statement);
-                match self.previous_status {
-                    FAILURE => self.previous_status = SUCCESS,
-                    SUCCESS => self.previous_status = FAILURE,
-                    _ => (),
-                }
-                let previous_status = self.previous_status.to_string();
-                self.variables_mut().set("?", previous_status);
+                self.previous_status.toggle();
+                self.variables.set("?", self.previous_status);
             }
             Statement::Break => return Condition::Break,
             Statement::Continue => return Condition::Continue,
@@ -257,7 +254,7 @@ impl<'a> Shell<'a> {
         }
         if let Some(signal) = signals::SignalHandler.next() {
             if self.handle_signal(signal) {
-                self.exit(Some(get_signal_code(signal)));
+                self.exit_with_code(Status::from_signal(signal));
             }
             Condition::SigInt
         } else if self.break_flow {
@@ -327,7 +324,7 @@ impl<'a> Shell<'a> {
 
                 if let Some(statement) = case.conditional.as_ref() {
                     self.on_command(statement);
-                    if self.previous_status != SUCCESS {
+                    if self.previous_status.is_failure() {
                         continue;
                     }
                 }
diff --git a/src/lib/shell/flow_control.rs b/src/lib/shell/flow_control.rs
index b275c96e7947e39131a787e0062798af307b99a0..69967e24c31edd2c9b289d4f9b0c1aa522e7ae0d 100644
--- a/src/lib/shell/flow_control.rs
+++ b/src/lib/shell/flow_control.rs
@@ -1,7 +1,7 @@
 use crate::{
     lexers::assignments::{KeyBuf, Operator, Primitive},
     parser::{assignments::*, pipelines::Pipeline},
-    shell::Shell,
+    shell::{status::Status, Shell},
     types,
 };
 use small;
@@ -104,7 +104,7 @@ pub enum Statement<'a> {
     },
     Else,
     End,
-    Error(i32),
+    Error(Status),
     Break,
     Continue,
     Pipeline(Pipeline<'a>),
@@ -502,7 +502,7 @@ mod tests {
     fn return_toplevel() {
         let mut flow_control = Block::default();
         let oks = vec![
-            Statement::Error(1),
+            Statement::Error(Status::from_exit_code(1)),
             Statement::Time(Box::new(Statement::Default)),
             Statement::And(Box::new(Statement::Default)),
             Statement::Or(Box::new(Statement::Default)),
diff --git a/src/lib/shell/fork.rs b/src/lib/shell/fork.rs
index 52a24bccabf419b707be70a652c1bb6d1ad53b13..941330fbbca77a4082dfb03de46f21e4696e6ae9 100644
--- a/src/lib/shell/fork.rs
+++ b/src/lib/shell/fork.rs
@@ -127,7 +127,7 @@ impl<'a, 'b> Fork<'a, 'b> {
 
                 // Execute the given closure within the child's shell.
                 child_func(&mut shell);
-                sys::fork_exit(shell.previous_status);
+                sys::fork_exit(shell.previous_status.as_os_code());
             }
             Ok(pid) => {
                 Ok(IonResult {
diff --git a/src/lib/shell/mod.rs b/src/lib/shell/mod.rs
index 1856e7bae342f63328692f3ceef9f8fb3a4cf2ca..ffd1f292e04a0f7c3d7e00ed058c783dded5463b 100644
--- a/src/lib/shell/mod.rs
+++ b/src/lib/shell/mod.rs
@@ -87,7 +87,7 @@ pub struct Shell<'a> {
     directory_stack: DirectoryStack,
     /// When a command is executed, the final result of that command is stored
     /// here.
-    previous_status: i32,
+    previous_status: Status,
     /// The job ID of the previous command sent to the background.
     previous_job: usize,
     /// Contains all the options relative to the shell
@@ -184,7 +184,7 @@ impl<'a> Shell<'a> {
             flow_control: Block::with_capacity(5),
             directory_stack: DirectoryStack::new(),
             previous_job: !0,
-            previous_status: SUCCESS,
+            previous_status: Status::SUCCESS,
             opts: ShellOptions {
                 err_exit: false,
                 print_comms: false,
@@ -265,7 +265,7 @@ impl<'a> Shell<'a> {
         &mut self,
         name: &str,
         args: &[S],
-    ) -> Result<i32, IonError> {
+    ) -> Result<Status, IonError> {
         if let Some(Value::Function(function)) = self.variables.get_ref(name).cloned() {
             function
                 .execute(self, args)
@@ -290,8 +290,7 @@ impl<'a> Shell<'a> {
     /// commands as they arrive
     pub fn execute_script<T: std::io::Read>(&mut self, lines: T) {
         if let Err(why) = self.execute_command(lines) {
-            eprintln!("ion: {}", why);
-            self.previous_status = FAILURE;
+            self.previous_status = Status::error(format!("ion: {}", why));
         }
     }
 
@@ -302,7 +301,7 @@ impl<'a> Shell<'a> {
     /// the command(s) in the command line REPL interface for Ion. If the supplied command is
     /// not
     /// terminated, then an error will be returned.
-    pub fn execute_command<T: std::io::Read>(&mut self, command: T) -> Result<i32, IonError> {
+    pub fn execute_command<T: std::io::Read>(&mut self, command: T) -> Result<Status, IonError> {
         for cmd in command
             .bytes()
             .filter_map(Result::ok)
@@ -312,7 +311,7 @@ impl<'a> Shell<'a> {
         }
 
         if let Some(block) = self.flow_control.last().map(Statement::short) {
-            self.previous_status = FAILURE;
+            self.previous_status = Status::from_exit_code(1);
             Err(IonError::UnclosedBlock { block: block.to_string() })
         } else {
             Ok(self.previous_status)
@@ -320,7 +319,7 @@ impl<'a> Shell<'a> {
     }
 
     /// Executes a pipeline and returns the final exit status of the pipeline.
-    pub fn run_pipeline(&mut self, mut pipeline: Pipeline<'a>) -> Option<i32> {
+    pub fn run_pipeline(&mut self, mut pipeline: Pipeline<'a>) -> Status {
         let command_start_time = SystemTime::now();
 
         pipeline.expand(self);
@@ -332,12 +331,12 @@ impl<'a> Shell<'a> {
                     eprintln!("> {}", pipeline.to_string());
                 }
                 if self.opts.no_exec {
-                    Some(SUCCESS)
+                    Status::SUCCESS
                 } else {
-                    Some(main(&pipeline.items[0].job.args, self))
+                    main(&pipeline.items[0].job.args, self)
                 }
             } else {
-                Some(self.execute_pipeline(pipeline))
+                self.execute_pipeline(pipeline)
             }
         // Branch else if -> input == shell function and set the exit_status
         } else if let Some(Value::Function(function)) =
@@ -345,25 +344,23 @@ impl<'a> Shell<'a> {
         {
             if !pipeline.requires_piping() {
                 match function.execute(self, &pipeline.items[0].job.args) {
-                    Ok(()) => None,
+                    Ok(()) => self.previous_status,
                     Err(FunctionError::InvalidArgumentCount) => {
-                        eprintln!("ion: invalid number of function arguments supplied");
-                        Some(FAILURE)
+                        Status::error("ion: invalid number of function arguments supplied")
                     }
                     Err(FunctionError::InvalidArgumentType(expected_type, value)) => {
-                        eprintln!(
+                        Status::error(format!(
                             "ion: function argument has invalid type: expected {}, found value \
                              \'{}\'",
                             expected_type, value
-                        );
-                        Some(FAILURE)
+                        ))
                     }
                 }
             } else {
-                Some(self.execute_pipeline(pipeline))
+                self.execute_pipeline(pipeline)
             }
         } else {
-            Some(self.execute_pipeline(pipeline))
+            self.execute_pipeline(pipeline)
         };
 
         if let Some(ref callback) = self.on_command {
@@ -373,10 +370,8 @@ impl<'a> Shell<'a> {
         }
 
         // Retrieve the exit_status and set the $? variable and history.previous_status
-        if let Some(code) = exit_status {
-            self.variables_mut().set("?", code.to_string());
-            self.previous_status = code;
-        }
+        self.variables_mut().set("?", exit_status);
+        self.previous_status = exit_status;
 
         exit_status
     }
@@ -473,12 +468,15 @@ impl<'a> Shell<'a> {
     pub fn suspend(&self) { signals::suspend(0); }
 
     /// Get the last command's return code and/or the code for the error
-    pub fn previous_status(&self) -> i32 { self.previous_status }
+    pub fn previous_status(&self) -> Status { self.previous_status }
 
     /// Cleanly exit ion
-    pub fn exit(&mut self, status: Option<i32>) -> ! {
+    pub fn exit(&mut self) -> ! { self.exit_with_code(self.previous_status) }
+
+    /// Cleanly exit ion with custom code
+    pub fn exit_with_code(&mut self, status: Status) -> ! {
         self.prep_for_exit();
-        process::exit(status.unwrap_or(self.previous_status));
+        process::exit(status.as_os_code());
     }
 
     pub fn assign(&mut self, key: &Key, value: Value<'a>) -> Result<(), String> {
diff --git a/src/lib/shell/pipe_exec/fork.rs b/src/lib/shell/pipe_exec/fork.rs
index 39929969d1323432a9258832b932b6b82b73f81a..43cb23d91d0642858a9e351d039bc4abd47fa08d 100644
--- a/src/lib/shell/pipe_exec/fork.rs
+++ b/src/lib/shell/pipe_exec/fork.rs
@@ -18,7 +18,7 @@ impl<'a> Shell<'a> {
         pipeline: Pipeline<'a>,
         command_name: String,
         state: ProcessState,
-    ) -> i32 {
+    ) -> Status {
         match unsafe { sys::fork() } {
             Ok(0) => {
                 self.opts_mut().is_background_shell = true;
@@ -31,18 +31,18 @@ impl<'a> Shell<'a> {
                 Self::create_process_group(0);
 
                 // After execution of it's commands, exit with the last command's status.
-                sys::fork_exit(self.pipe(pipeline));
+                sys::fork_exit(self.pipe(pipeline).as_os_code());
             }
             Ok(pid) => {
                 if state != ProcessState::Empty {
                     // The parent process should add the child fork's PID to the background.
                     self.send_to_background(BackgroundProcess::new(pid, state, command_name));
                 }
-                SUCCESS
+                Status::SUCCESS
             }
             Err(why) => {
                 eprintln!("ion: background fork failed: {}", why);
-                exit(FAILURE);
+                exit(1);
             }
         }
     }
diff --git a/src/lib/shell/pipe_exec/job_control.rs b/src/lib/shell/pipe_exec/job_control.rs
index f1f873d6105d78cfd21af0231414f8fb974d4105..a3d362aa67917d2101bcd2dcffdafb351a013577 100644
--- a/src/lib/shell/pipe_exec/job_control.rs
+++ b/src/lib/shell/pipe_exec/job_control.rs
@@ -151,7 +151,7 @@ impl<'a> Shell<'a> {
 
                     get_process!(|process| {
                         if fg_was_grabbed {
-                            fg.reply_with(TERMINATED as i8);
+                            fg.reply_with(Status::TERMINATED.as_os_code() as i8);
                         }
                         process.state = ProcessState::Stopped;
                     });
@@ -203,23 +203,24 @@ impl<'a> Shell<'a> {
         }
     }
 
-    pub fn watch_foreground(&mut self, pgid: u32) -> i32 {
-        let mut signaled = 0;
-        let mut exit_status = 0;
+    pub fn watch_foreground(&mut self, pgid: u32) -> Status {
+        let mut signaled = Status::SUCCESS;
+        let mut exit_status = Status::SUCCESS;
 
         loop {
             let mut status = 0;
             match waitpid(-(pgid as i32), &mut status, WUNTRACED) {
                 Err(errno) => match errno {
-                    ECHILD if signaled == 0 => break exit_status,
+                    ECHILD if signaled.is_success() => break exit_status,
                     ECHILD => break signaled,
                     errno => {
-                        eprintln!("ion: waitpid error: {}", strerror(errno));
-                        break FAILURE;
+                        break Status::error(format!("ion: waitpid error: {}", strerror(errno)))
                     }
                 },
                 Ok(0) => (),
-                Ok(_) if wifexited(status) => exit_status = wexitstatus(status),
+                Ok(_) if wifexited(status) => {
+                    exit_status = Status::from_exit_code(wexitstatus(status))
+                }
                 Ok(pid) if wifsignaled(status) => {
                     let signal = wtermsig(status);
                     if signal == SIGPIPE {
@@ -236,7 +237,7 @@ impl<'a> Shell<'a> {
                                 self.handle_signal(signal);
                             }
                         }
-                        signaled = 128 + signal as i32;
+                        signaled = Status::from_signal(signal as i32);
                     }
                 }
                 Ok(pid) if wifstopped(status) => {
@@ -246,7 +247,7 @@ impl<'a> Shell<'a> {
                         "".to_string(),
                     ));
                     self.break_flow = true;
-                    break 128 + wstopsig(status);
+                    break Status::from_signal(wstopsig(status));
                 }
                 Ok(_) => (),
             }
@@ -259,7 +260,7 @@ impl<'a> Shell<'a> {
         while self.background.lock().unwrap().iter().any(|p| p.state == ProcessState::Running) {
             if let Some(signal) = signals::SignalHandler.find(|&s| s != sys::SIGTSTP) {
                 self.background_send(signal);
-                self.exit(Some(get_signal_code(signal)));
+                self.exit_with_code(Status::from_signal(signal));
             }
             sleep(Duration::from_millis(100));
         }
@@ -276,7 +277,7 @@ impl<'a> Shell<'a> {
     /// Takes a background tasks's PID and whether or not it needs to be continued; resumes the
     /// task and sets it as the foreground process. Once the task exits or stops, the exit status
     /// will be returned, and ownership of the TTY given back to the shell.
-    pub fn set_bg_task_in_foreground(&self, pid: u32, cont: bool) -> i32 {
+    pub fn set_bg_task_in_foreground(&self, pid: u32, cont: bool) -> Status {
         // Pass the TTY to the background job
         Self::set_foreground_as(pid);
         // Signal the background thread that is waiting on this process to stop waiting.
@@ -291,8 +292,8 @@ impl<'a> Shell<'a> {
             // signal, the status of that process will be communicated back. To
             // avoid consuming CPU cycles, we wait 25 ms between polls.
             match self.foreground_signals.was_processed() {
-                Some(BackgroundResult::Status(stat)) => break i32::from(stat),
-                Some(BackgroundResult::Errored) => break TERMINATED,
+                Some(BackgroundResult::Status(stat)) => break Status::from_exit_code(stat as i32),
+                Some(BackgroundResult::Errored) => break Status::TERMINATED,
                 None => sleep(Duration::from_millis(25)),
             }
         };
diff --git a/src/lib/shell/pipe_exec/mod.rs b/src/lib/shell/pipe_exec/mod.rs
index 358c07ef2d7d7363449c678a58db059980c92d2c..8fc371116becd19316691c1ded9b1c5dba8884a8 100644
--- a/src/lib/shell/pipe_exec/mod.rs
+++ b/src/lib/shell/pipe_exec/mod.rs
@@ -20,7 +20,7 @@ use super::{
     flow_control::FunctionError,
     job::{Job, JobVariant, RefinedJob, TeeItem},
     signals::{self, SignalHandler},
-    status::*,
+    status::Status,
     Shell, Value,
 };
 use crate::{
@@ -187,7 +187,7 @@ impl<'b> Shell<'b> {
         stdin: &Option<File>,
         stdout: &Option<File>,
         stderr: &Option<File>,
-    ) -> i32 {
+    ) -> Status {
         let result = sys::fork_and_exec(
             name,
             args,
@@ -208,12 +208,9 @@ impl<'b> Shell<'b> {
             }
             Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
                 self.command_not_found(name);
-                NO_SUCH_COMMAND
-            }
-            Err(ref err) => {
-                eprintln!("ion: command exec error: {}", err);
-                FAILURE
+                Status::NO_SUCH_COMMAND
             }
+            Err(ref err) => Status::error(format!("ion: command exec error: {}", err)),
         }
     }
 
@@ -222,7 +219,7 @@ impl<'b> Shell<'b> {
         &mut self,
         items: &mut (Option<TeeItem>, Option<TeeItem>),
         redirection: RedirectFrom,
-    ) -> i32 {
+    ) -> Status {
         let res = match *items {
             (None, None) => panic!("There must be at least one TeeItem, this is a bug"),
             (Some(ref mut tee_out), None) => match redirection {
@@ -239,40 +236,39 @@ impl<'b> Shell<'b> {
             }
         };
         if let Err(e) = res {
-            eprintln!("ion: error in multiple output redirection process: {:?}", e);
-            FAILURE
+            Status::error(format!("ion: error in multiple output redirection process: {:?}", e))
         } else {
-            SUCCESS
+            Status::SUCCESS
         }
     }
 
     /// For cat jobs
-    fn exec_multi_in(&mut self, sources: &mut [File], stdin: &mut Option<File>) -> i32 {
+    fn exec_multi_in(&mut self, sources: &mut [File], stdin: &mut Option<File>) -> Status {
         let stdout = io::stdout();
         let mut stdout = stdout.lock();
         for file in stdin.iter_mut().chain(sources) {
             if let Err(why) = std::io::copy(file, &mut stdout) {
-                eprintln!("ion: error in multiple input redirect process: {:?}", why);
-                return FAILURE;
+                return Status::error(format!(
+                    "ion: error in multiple input redirect process: {:?}",
+                    why
+                ));
             }
         }
-        SUCCESS
+        Status::SUCCESS
     }
 
-    fn exec_function<S: AsRef<str>>(&mut self, name: &str, args: &[S]) -> i32 {
+    fn exec_function<S: AsRef<str>>(&mut self, name: &str, args: &[S]) -> Status {
         if let Some(Value::Function(function)) = self.variables.get_ref(name).cloned() {
             match function.execute(self, args) {
-                Ok(()) => SUCCESS,
+                Ok(()) => Status::SUCCESS,
                 Err(FunctionError::InvalidArgumentCount) => {
-                    eprintln!("ion: invalid number of function arguments supplied");
-                    FAILURE
+                    Status::error(format!("ion: invalid number of function arguments supplied"))
                 }
                 Err(FunctionError::InvalidArgumentType(expected_type, value)) => {
-                    eprintln!(
+                    Status::error(format!(
                         "ion: function argument has invalid type: expected {}, found value \'{}\'",
                         expected_type, value
-                    );
-                    FAILURE
+                    ))
                 }
             }
         } else {
@@ -286,7 +282,7 @@ impl<'b> Shell<'b> {
     /// * `name`: Name of the builtin to execute.
     /// * `stdin`, `stdout`, `stderr`: File descriptors that will replace the respective standard
     ///   streams if they are not `None`
-    fn exec_builtin<'a>(&mut self, main: BuiltinFunction<'a>, args: &[small::String]) -> i32 {
+    fn exec_builtin<'a>(&mut self, main: BuiltinFunction<'a>, args: &[small::String]) -> Status {
         main(args, self)
     }
 
@@ -294,7 +290,7 @@ impl<'b> Shell<'b> {
     ///
     /// The aforementioned `RefinedJob` may be either a builtin or external command.
     /// The purpose of this function is therefore to execute both types accordingly.
-    fn exec_job(&mut self, job: &RefinedJob<'b>) -> i32 {
+    fn exec_job(&mut self, job: &RefinedJob<'b>) -> Status {
         // Duplicate file descriptors, execute command, and redirect back.
         if let Ok((stdin_bk, stdout_bk, stderr_bk)) = duplicate_streams() {
             redirect_streams(&job.stdin, &job.stdout, &job.stderr);
@@ -314,7 +310,7 @@ impl<'b> Shell<'b> {
                 job.long()
             );
 
-            COULD_NOT_EXEC
+            Status::COULD_NOT_EXEC
         }
     }
 
@@ -353,10 +349,10 @@ impl<'b> Shell<'b> {
     /// If a job is stopped, the shell will add that job to a list of background jobs and
     /// continue to watch the job in the background, printing notifications on status changes
     /// of that job over time.
-    pub fn execute_pipeline(&mut self, pipeline: Pipeline<'b>) -> i32 {
+    pub fn execute_pipeline(&mut self, pipeline: Pipeline<'b>) -> Status {
         // Don't execute commands when the `-n` flag is passed.
         if self.opts.no_exec {
-            return SUCCESS;
+            return Status::SUCCESS;
         }
 
         // A string representing the command is stored here.
@@ -387,10 +383,10 @@ impl<'b> Shell<'b> {
     /// Executes a piped job `job1 | job2 | job3`
     ///
     /// This function will panic if called with an empty slice
-    fn pipe(&mut self, pipeline: Pipeline<'b>) -> i32 {
+    fn pipe(&mut self, pipeline: Pipeline<'b>) -> Status {
         let mut commands = match prepare(self, pipeline) {
             Ok(c) => c.into_iter().peekable(),
-            Err(_) => return COULD_NOT_EXEC,
+            Err(_) => return Status::COULD_NOT_EXEC,
         };
 
         if let Some((mut parent, mut kind)) = commands.next() {
@@ -468,7 +464,7 @@ impl<'b> Shell<'b> {
                 // returning the exit status of the last process in the queue.
                 // Watch the foreground group, dropping all commands that exit as they exit.
                 let status = self.watch_foreground(pgid);
-                if status == TERMINATED {
+                if status == Status::TERMINATED {
                     if let Err(why) = sys::killpg(pgid, sys::SIGTERM) {
                         eprintln!("ion: failed to terminate foreground jobs: {}", why);
                     }
@@ -486,7 +482,7 @@ impl<'b> Shell<'b> {
                 status
             }
         } else {
-            SUCCESS
+            Status::SUCCESS
         }
     }
 }
@@ -592,7 +588,7 @@ fn fork_exec_internal<F>(
     pgid: u32,
     mut exec_action: F,
 ) where
-    F: FnMut(Option<File>, Option<File>, Option<File>) -> i32,
+    F: FnMut(Option<File>, Option<File>, Option<File>) -> Status,
 {
     match unsafe { sys::fork() } {
         Ok(0) => {
@@ -600,7 +596,7 @@ fn fork_exec_internal<F>(
 
             redirect_streams(&stdin, &stdout, &stderr);
             let exit_status = exec_action(stdout, stderr, stdin);
-            exit(exit_status)
+            exit(exit_status.as_os_code())
         }
         Ok(pid) => {
             *last_pid = *current_pid;
diff --git a/src/lib/shell/shell_expand.rs b/src/lib/shell/shell_expand.rs
index e327ad5d16a1a631d9089f2920fc7ea9e7130b34..8d458fe66e8e9ef140c37c811f183f2baeec82c9 100644
--- a/src/lib/shell/shell_expand.rs
+++ b/src/lib/shell/shell_expand.rs
@@ -36,7 +36,7 @@ impl<'a, 'b> Expander for Shell<'b> {
     /// Expand a string variable given if its quoted / unquoted
     fn string(&self, name: &str) -> Option<types::Str> {
         if name == "?" {
-            Some(types::Str::from(self.previous_status.to_string()))
+            Some(self.previous_status.into())
         } else {
             self.variables().get_str(name)
         }
diff --git a/src/lib/shell/status.rs b/src/lib/shell/status.rs
index 65d0b1ef980c9dc25fbbd08d9fb91aa77e1b6c90..70cc8ed002a50daa9d06613de060518b83042173 100644
--- a/src/lib/shell/status.rs
+++ b/src/lib/shell/status.rs
@@ -1,8 +1,42 @@
-pub const SUCCESS: i32 = 0;
-pub const FAILURE: i32 = 1;
-pub const BAD_ARG: i32 = 2;
-pub const COULD_NOT_EXEC: i32 = 126;
-pub const NO_SUCH_COMMAND: i32 = 127;
-pub const TERMINATED: i32 = 143;
-
-pub fn get_signal_code(signal: i32) -> i32 { 128 + signal }
+use super::{super::types, Value};
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
+pub struct Status(i32);
+
+impl Status {
+    pub const BAD_ARG: Self = Status(2);
+    pub const COULD_NOT_EXEC: Self = Status(126);
+    pub const NO_SUCH_COMMAND: Self = Status(127);
+    pub const SUCCESS: Self = Status(0);
+    pub const TERMINATED: Self = Status(143);
+
+    pub fn from_signal(signal: i32) -> Self { Status(128 + signal) }
+
+    pub fn from_exit_code(code: i32) -> Self { Status(code) }
+
+    pub fn from_bool(b: bool) -> Self { Status(!b as i32) }
+
+    pub fn error<T: AsRef<str>>(err: T) -> Self {
+        let err = err.as_ref();
+        if !err.is_empty() {
+            eprintln!("{}", err);
+        }
+        Status(1)
+    }
+
+    pub fn is_success(&self) -> bool { self.0 == 0 }
+
+    pub fn is_failure(&self) -> bool { self.0 != 0 }
+
+    pub fn as_os_code(&self) -> i32 { self.0 }
+
+    pub fn toggle(&mut self) { self.0 = if self.is_success() { 1 } else { 0 }; }
+}
+
+impl<'a> From<Status> for Value<'a> {
+    fn from(status: Status) -> Self { Value::Str(status.into()) }
+}
+
+impl From<Status> for types::Str {
+    fn from(status: Status) -> Self { types::Str::from(status.as_os_code().to_string()) }
+}
diff --git a/src/main.rs b/src/main.rs
index 45af677cbde58ed17ec78b8dc1d54a19aaba77dc..8fb6c765aee886a95812acfbdb61888e8e81d344 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -86,5 +86,5 @@ fn main() {
         shell.execute_script(BufReader::new(stdin()));
     }
     shell.wait_for_background();
-    shell.exit(None);
+    shell.exit();
 }