diff --git a/examples/basic_sleep.rs b/examples/basic_sleep.rs
index bc14ac9..dc27d3b 100644
--- a/examples/basic_sleep.rs
+++ b/examples/basic_sleep.rs
@@ -23,7 +23,7 @@ pub fn main() -> LuaResult<()> {
     // Load the main script into a runtime and run it until completion
     let rt = Runtime::new(&lua)?;
     let main = lua.load(MAIN_SCRIPT);
-    rt.push_main(&lua, main, ());
+    rt.push_thread(&lua, main, ());
     rt.run_blocking(&lua);
 
     Ok(())
diff --git a/examples/basic_spawn.rs b/examples/basic_spawn.rs
index ca0ad0d..66f1f8e 100644
--- a/examples/basic_spawn.rs
+++ b/examples/basic_spawn.rs
@@ -29,7 +29,7 @@ pub fn main() -> LuaResult<()> {
     // Load the main script into a runtime and run it until completion
     let rt = Runtime::new(&lua)?;
     let main = lua.load(MAIN_SCRIPT);
-    rt.push_main(&lua, main, ());
+    rt.push_thread(&lua, main, ());
     rt.run_blocking(&lua);
 
     Ok(())
diff --git a/examples/callbacks.rs b/examples/callbacks.rs
index 7cc074e..ba0d473 100644
--- a/examples/callbacks.rs
+++ b/examples/callbacks.rs
@@ -9,15 +9,22 @@ pub fn main() -> LuaResult<()> {
     // Set up persistent lua environment
     let lua = Lua::new();
 
-    // Load the main script into a runtime
+    // Create a new runtime with custom callbacks
     let rt = Runtime::new(&lua)?;
+    rt.set_callbacks(
+        &lua,
+        Callbacks::default().on_error(|_, _, e| {
+            println!(
+                "Captured error from Lua!\n{}\n{e}\n{}",
+                "-".repeat(15),
+                "-".repeat(15)
+            );
+        }),
+    );
+
+    // Load and run the main script until completion
     let main = lua.load(MAIN_SCRIPT);
-
-    // Inject default value & error callbacks - this will print lua errors to stderr
-    Callbacks::default().inject(&lua);
-
-    // Run the main script until completion
-    rt.push_main(&lua, main, ());
+    rt.push_thread(&lua, main, ());
     rt.run_blocking(&lua);
 
     Ok(())
diff --git a/examples/main.rs b/examples/main.rs
index f1ed537..0181e15 100644
--- a/examples/main.rs
+++ b/examples/main.rs
@@ -45,28 +45,30 @@ pub fn main() -> LuaResult<()> {
 fn run<'lua>(lua: &'lua Lua, main: impl IntoLuaThread<'lua>) -> LuaResult<LuaValue> {
     // Set up runtime (thread queue / async executors)
     let rt = Runtime::new(lua)?;
-    let thread = rt.push_main(lua, main, ());
+    let thread = rt.push_thread(lua, main, ());
     lua.set_named_registry_value("mainThread", thread)?;
 
-    // Add callbacks to capture resulting value/error of main thread,
-    // we need to do some tricks to get around lifetime issues with 'lua
-    // being different inside the callback vs outside the callback for LuaValue
+    // Create callbacks to capture resulting value/error of main thread,
+    // we need to do some tricks to get around the lifetime issues with 'lua
+    // being different inside the callback vs. outside the callback, for LuaValue
     let captured_error = Rc::new(Mutex::new(None));
     let captured_error_inner = Rc::clone(&captured_error);
-    Callbacks::new()
-        .on_value(|lua, thread, val| {
-            let main: LuaThread = lua.named_registry_value("mainThread").unwrap();
-            if main == thread {
-                lua.set_named_registry_value("mainValue", val).unwrap();
-            }
-        })
-        .on_error(move |lua, thread, err| {
-            let main: LuaThread = lua.named_registry_value("mainThread").unwrap();
-            if main == thread {
-                captured_error_inner.lock_blocking().replace(err);
-            }
-        })
-        .inject(lua);
+    rt.set_callbacks(
+        lua,
+        Callbacks::new()
+            .on_value(|lua, thread, val| {
+                let main: LuaThread = lua.named_registry_value("mainThread").unwrap();
+                if main == thread {
+                    lua.set_named_registry_value("mainValue", val).unwrap();
+                }
+            })
+            .on_error(move |lua, thread, err| {
+                let main: LuaThread = lua.named_registry_value("mainThread").unwrap();
+                if main == thread {
+                    captured_error_inner.lock_blocking().replace(err);
+                }
+            }),
+    );
 
     // Run until end
     rt.run_blocking(lua);
diff --git a/examples/scheduler_ordering.rs b/examples/scheduler_ordering.rs
index 78f2786..3e663fd 100644
--- a/examples/scheduler_ordering.rs
+++ b/examples/scheduler_ordering.rs
@@ -24,7 +24,7 @@ pub fn main() -> LuaResult<()> {
     // Load the main script into a runtime and run it until completion
     let rt = Runtime::new(&lua)?;
     let main = lua.load(MAIN_SCRIPT);
-    rt.push_main(&lua, main, ());
+    rt.push_thread(&lua, main, ());
     rt.run_blocking(&lua);
 
     Ok(())
diff --git a/lib/callbacks.rs b/lib/callbacks.rs
index 1f85048..7b86de0 100644
--- a/lib/callbacks.rs
+++ b/lib/callbacks.rs
@@ -6,16 +6,34 @@ type ErrorCallback = Box<dyn for<'lua> Fn(&'lua Lua, LuaThread<'lua>, LuaError)
 const FORWARD_VALUE_KEY: &str = "__runtime__forwardValue";
 const FORWARD_ERROR_KEY: &str = "__runtime__forwardError";
 
+/**
+    A set of callbacks for thread values and errors.
+
+    These callbacks are used to forward values and errors from
+    Lua threads back to Rust. By default, the runtime will print
+    any errors to stderr and not do any operations with values.
+
+    You can set your own callbacks using the `on_value` and `on_error` builder methods.
+*/
 pub struct Callbacks {
     on_value: Option<ValueCallback>,
     on_error: Option<ErrorCallback>,
 }
 
 impl Callbacks {
-    pub fn new() -> Callbacks {
-        Default::default()
+    /**
+        Creates a new set of callbacks with no callbacks set.
+    */
+    pub fn new() -> Self {
+        Self {
+            on_value: None,
+            on_error: None,
+        }
     }
 
+    /**
+        Sets the callback for thread values being yielded / returned.
+    */
     pub fn on_value<F>(mut self, f: F) -> Self
     where
         F: Fn(&Lua, LuaThread, LuaValue) + 'static,
@@ -24,6 +42,9 @@ impl Callbacks {
         self
     }
 
+    /**
+        Sets the callback for thread errors.
+    */
     pub fn on_error<F>(mut self, f: F) -> Self
     where
         F: Fn(&Lua, LuaThread, LuaError) + 'static,
@@ -32,17 +53,27 @@ impl Callbacks {
         self
     }
 
+    /**
+        Removes any current thread value callback.
+    */
     pub fn without_value_callback(mut self) -> Self {
         self.on_value.take();
         self
     }
 
+    /**
+        Removes any current thread error callback.
+    */
     pub fn without_error_callback(mut self) -> Self {
         self.on_error.take();
         self
     }
 
-    pub fn inject(self, lua: &Lua) {
+    pub(crate) fn inject(self, lua: &Lua) {
+        // Remove any previously injected callbacks
+        lua.unset_named_registry_value(FORWARD_VALUE_KEY).ok();
+        lua.unset_named_registry_value(FORWARD_ERROR_KEY).ok();
+
         // Create functions to forward values & errors
         if let Some(f) = self.on_value {
             lua.set_named_registry_value(
diff --git a/lib/runtime.rs b/lib/runtime.rs
index 3d1f4aa..be8bde2 100644
--- a/lib/runtime.rs
+++ b/lib/runtime.rs
@@ -29,7 +29,8 @@ impl Runtime {
     /**
         Creates a new runtime for the given Lua state.
 
-        This will inject some functions to interact with the scheduler / executor.
+        This will inject some functions to interact with the scheduler / executor,
+        as well as the default [`Callbacks`] for thread values and errors.
     */
     pub fn new(lua: &Lua) -> LuaResult<Runtime> {
         let queue_status = Rc::new(Cell::new(false));
@@ -106,6 +107,9 @@ impl Runtime {
         lua.globals().set(GLOBAL_NAME_SPAWN, fn_spawn)?;
         lua.globals().set(GLOBAL_NAME_DEFER, fn_defer)?;
 
+        // Finally, inject default callbacks
+        Callbacks::default().inject(lua);
+
         Ok(Runtime {
             queue_status,
             queue_spawn,
@@ -116,9 +120,20 @@ impl Runtime {
     }
 
     /**
-        Pushes a chunk / function / thread to the front of the runtime.
+        Sets the callbacks for this runtime.
+
+        This will overwrite any previously set callbacks, including default ones.
     */
-    pub fn push_main<'lua>(
+    pub fn set_callbacks(&self, lua: &Lua, callbacks: Callbacks) {
+        callbacks.inject(lua);
+    }
+
+    /**
+        Pushes a chunk / function / thread to the runtime queue.
+
+        Threads are guaranteed to be resumed in the order that they were pushed to the queue.
+    */
+    pub fn push_thread<'lua>(
         &self,
         lua: &'lua Lua,
         thread: impl IntoLuaThread<'lua>,