use thiserror::Error;
 
-/// Errors originating from the M17 Rust Toolkit core
+/// Errors from the M17 Rust Toolkit
 #[derive(Debug, Error)]
 pub enum M17Error {
     #[error("given callsign contains at least one character invalid in M17: {0}")]
 
     #[error("adapter error for id {0}: {1}")]
     Adapter(usize, #[source] AdapterError),
+
+    #[error("soundmodem component error: {0}")]
+    Soundmodem(#[source] SoundmodemError),
 }
 
+/// Arbitrary error type returned from adapters, which may be user-implemented
 pub type AdapterError = Box<dyn std::error::Error + Sync + Send + 'static>;
 
+/// Arbitrary error type returned from soundmodem components, which may be user-implemented
+pub type SoundmodemError = Box<dyn std::error::Error + Sync + Send + 'static>;
+
 /// Iterator over potentially multiple errors
 #[derive(Debug, Error)]
 pub struct M17Errors(pub(crate) Vec<M17Error>);
 
 impl Display for M17Errors {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{:?}", self.0)
+        let mut displays = vec![];
+        for e in &self.0 {
+            displays.push(e.to_string());
+        }
+        write!(f, "[{}]", displays.join(", "))
     }
 }
+
+#[derive(Debug, Error)]
+pub enum M17SoundmodemError {}
 
 };
 
 use crate::{
-    error::M17Error,
+    error::{M17Error, SoundmodemError},
     soundmodem::{InputSource, SoundmodemEvent},
 };
 
 }
 
 impl InputSource for RtlSdr {
-    fn start(&self, tx: SyncSender<SoundmodemEvent>) {
-        // TODO: error handling
+    fn start(&self, tx: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
         let mut cmd = Command::new("rtl_fm")
             .args([
                 "-E",
                 "48k",
             ])
             .stdout(Stdio::piped())
-            .spawn()
-            .unwrap();
+            .spawn()?;
         let mut stdout = cmd.stdout.take().unwrap();
         let mut buf = [0u8; 1024];
         let mut leftover: Option<u8> = None;
             }
         });
         *self.rtlfm.lock().unwrap() = Some(cmd);
+        Ok(())
     }
 
-    fn close(&self) {
+    fn close(&self) -> Result<(), SoundmodemError> {
         if let Some(mut process) = self.rtlfm.lock().unwrap().take() {
             let _ = process.kill();
         }
+        Ok(())
     }
 }
 
 use serialport::SerialPort;
 
-use crate::soundmodem::Ptt;
+use crate::{error::SoundmodemError, soundmodem::Ptt};
 
 /// The pin on the serial port which is driving PTT
 pub enum PttPin {
             .map(|i| i.port_name)
     }
 
-    pub fn new(port_name: &str, pin: PttPin) -> Self {
+    pub fn new(port_name: &str, pin: PttPin) -> Result<Self, SoundmodemError> {
         // TODO: error handling
         let port = serialport::new(port_name, 9600).open().unwrap();
         let mut s = Self { port, pin };
-        s.ptt_off();
-        s
+        s.ptt_off()?;
+        Ok(s)
     }
 }
 
 impl Ptt for SerialPtt {
-    fn ptt_on(&mut self) {
-        let _ = match self.pin {
+    fn ptt_on(&mut self) -> Result<(), SoundmodemError> {
+        Ok(match self.pin {
             PttPin::Rts => self.port.write_request_to_send(true),
             PttPin::Dtr => self.port.write_data_terminal_ready(true),
-        };
+        }?)
     }
 
-    fn ptt_off(&mut self) {
-        let _ = match self.pin {
+    fn ptt_off(&mut self) -> Result<(), SoundmodemError> {
+        Ok(match self.pin {
             PttPin::Rts => self.port.write_request_to_send(false),
             PttPin::Dtr => self.port.write_data_terminal_ready(false),
-        };
+        }?)
     }
 }
 
 };
 
 use crate::{
-    error::M17Error,
+    error::{M17Error, SoundmodemError},
     soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent},
 };
 
 }
 
 impl InputSource for SoundcardInputSource {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) {
-        let _ = self.event_tx.send(SoundcardEvent::StartInput { samples });
+    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
+        Ok(self.event_tx.send(SoundcardEvent::StartInput { samples })?)
     }
 
-    fn close(&self) {
-        let _ = self.event_tx.send(SoundcardEvent::CloseInput);
+    fn close(&self) -> Result<(), SoundmodemError> {
+        Ok(self.event_tx.send(SoundcardEvent::CloseInput)?)
     }
 }
 
 }
 
 impl OutputSink for SoundcardOutputSink {
-    fn start(&self, event_tx: SyncSender<SoundmodemEvent>, buffer: Arc<RwLock<OutputBuffer>>) {
-        let _ = self
+    fn start(
+        &self,
+        event_tx: SyncSender<SoundmodemEvent>,
+        buffer: Arc<RwLock<OutputBuffer>>,
+    ) -> Result<(), SoundmodemError> {
+        Ok(self
             .event_tx
-            .send(SoundcardEvent::StartOutput { event_tx, buffer });
+            .send(SoundcardEvent::StartOutput { event_tx, buffer })?)
     }
 
-    fn close(&self) {
-        let _ = self.event_tx.send(SoundcardEvent::CloseOutput);
+    fn close(&self) -> Result<(), SoundmodemError> {
+        Ok(self.event_tx.send(SoundcardEvent::CloseOutput)?)
     }
 }
 
 
-use crate::error::M17Error;
+use crate::error::{M17Error, SoundmodemError};
 use crate::tnc::{Tnc, TncError};
 use log::debug;
 use m17core::kiss::MAX_FRAME_LEN;
                     tnc.set_data_carrier_detect(demodulator.data_carrier_detect());
                 }
                 SoundmodemEvent::Start => {
-                    input.start(event_tx.clone());
-                    output.start(event_tx.clone(), out_buffer.clone());
+                    // TODO: runtime event handling
+                    input.start(event_tx.clone()).unwrap();
+                    output.start(event_tx.clone(), out_buffer.clone()).unwrap();
                 }
                 SoundmodemEvent::Close => {
-                    ptt_driver.ptt_off();
+                    ptt_driver.ptt_off().unwrap();
                     break;
                 }
                 SoundmodemEvent::DidReadFromOutputBuffer { len, timestamp } => {
             let new_ptt = tnc.ptt();
             if new_ptt != ptt {
                 if new_ptt {
-                    ptt_driver.ptt_on();
+                    ptt_driver.ptt_on().unwrap();
                 } else {
-                    ptt_driver.ptt_off();
+                    ptt_driver.ptt_off().unwrap();
                 }
             }
             ptt = new_ptt;
 }
 
 pub trait InputSource: Send + Sync + 'static {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>);
-    fn close(&self);
+    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError>;
+    fn close(&self) -> Result<(), SoundmodemError>;
 }
 
 pub struct InputRrcFile {
 }
 
 impl InputSource for InputRrcFile {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) {
+    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
         let (end_tx, end_rx) = channel();
         let baseband = self.baseband.clone();
         std::thread::spawn(move || {
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
+        Ok(())
     }
 
-    fn close(&self) {
+    fn close(&self) -> Result<(), SoundmodemError> {
         let _ = self.end_tx.lock().unwrap().take();
+        Ok(())
     }
 }
 
 }
 
 impl InputSource for NullInputSource {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) {
+    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
         let (end_tx, end_rx) = channel();
         std::thread::spawn(move || {
             // assuming 48 kHz for now
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
+        Ok(())
     }
 
-    fn close(&self) {
+    fn close(&self) -> Result<(), SoundmodemError> {
         let _ = self.end_tx.lock().unwrap().take();
+        Ok(())
     }
 }
 
 }
 
 pub trait OutputSink: Send + Sync + 'static {
-    fn start(&self, event_tx: SyncSender<SoundmodemEvent>, buffer: Arc<RwLock<OutputBuffer>>);
-    fn close(&self);
+    fn start(
+        &self,
+        event_tx: SyncSender<SoundmodemEvent>,
+        buffer: Arc<RwLock<OutputBuffer>>,
+    ) -> Result<(), SoundmodemError>;
+    fn close(&self) -> Result<(), SoundmodemError>;
 }
 
 pub struct OutputRrcFile {
 }
 
 impl OutputSink for OutputRrcFile {
-    fn start(&self, event_tx: SyncSender<SoundmodemEvent>, buffer: Arc<RwLock<OutputBuffer>>) {
+    fn start(
+        &self,
+        event_tx: SyncSender<SoundmodemEvent>,
+        buffer: Arc<RwLock<OutputBuffer>>,
+    ) -> Result<(), SoundmodemError> {
         let (end_tx, end_rx) = channel();
-        let path = self.path.clone();
+        let mut file = File::create(self.path.clone())?;
         std::thread::spawn(move || {
-            // TODO: error handling
-            let mut file = File::create(path).unwrap();
-
             // assuming 48 kHz for now
             const TICK: Duration = Duration::from_millis(25);
             const SAMPLES_PER_TICK: usize = 1200;
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
+        Ok(())
     }
 
-    fn close(&self) {
+    fn close(&self) -> Result<(), SoundmodemError> {
         let _ = self.end_tx.lock().unwrap().take();
+        Ok(())
     }
 }
 
 }
 
 impl OutputSink for NullOutputSink {
-    fn start(&self, event_tx: SyncSender<SoundmodemEvent>, buffer: Arc<RwLock<OutputBuffer>>) {
+    fn start(
+        &self,
+        event_tx: SyncSender<SoundmodemEvent>,
+        buffer: Arc<RwLock<OutputBuffer>>,
+    ) -> Result<(), SoundmodemError> {
         let (end_tx, end_rx) = channel();
         std::thread::spawn(move || {
             // assuming 48 kHz for now
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
+        Ok(())
     }
 
-    fn close(&self) {
+    fn close(&self) -> Result<(), SoundmodemError> {
         let _ = self.end_tx.lock().unwrap().take();
+        Ok(())
     }
 }
 
 pub trait Ptt: Send + 'static {
-    fn ptt_on(&mut self);
-    fn ptt_off(&mut self);
+    fn ptt_on(&mut self) -> Result<(), SoundmodemError>;
+    fn ptt_off(&mut self) -> Result<(), SoundmodemError>;
 }
 
 /// There is no PTT because this TNC will never make transmissions on a real radio.
 }
 
 impl Ptt for NullPtt {
-    fn ptt_on(&mut self) {}
-    fn ptt_off(&mut self) {}
+    fn ptt_on(&mut self) -> Result<(), SoundmodemError> {
+        Ok(())
+    }
+
+    fn ptt_off(&mut self) -> Result<(), SoundmodemError> {
+        Ok(())
+    }
 }
 
         std::thread::spawn(move || stream_thread(end_rx, setup_tx, state, output_card));
         self.state.lock().unwrap().end_tx = Some(end_tx);
         // Propagate any errors arising in the thread
-        Ok(setup_rx.recv()??)
+        setup_rx.recv()?
     }
 
     fn close(&self) -> Result<(), AdapterError> {
 
 pub fn mod_test() {
     let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
     soundcard.set_tx_inverted(true);
-    let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts);
+    let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts).unwrap();
     let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt);
     let app = M17App::new(soundmodem);
     app.start().unwrap();
 
 fn main() {
     let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
     soundcard.set_tx_inverted(true);
-    let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts);
+    let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts).unwrap();
     let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt);
     let app = M17App::new(soundmodem);