let _ = data;
     }
 
-    // TODO
-    // fn stream_lost(&self);
+    // TODO: callbacks for LICH metadata received
     // fn stream_assembled_text_block()
     // fn stream_gnss_data()
     // fn stream_extended_callsign_data()
 
         while let Ok(ev) = event_rx.recv() {
             match ev {
                 TncControlEvent::Kiss(k) => {
-                    if let Err(e) = tnc.write_all(k.as_bytes()) {
-                        debug!("kiss send err: {:?}", e);
+                    if tnc.write_all(k.as_bytes()).is_err() {
                         return;
                     }
                 }
                 TncControlEvent::Start => {
-                    if let Err(e) = tnc.start() {
-                        debug!("tnc start err: {:?}", e);
-                        return;
-                    }
+                    tnc.start();
                 }
                 TncControlEvent::Close => {
-                    if let Err(e) = tnc.close() {
-                        debug!("tnc close err: {:?}", e);
-                        return;
-                    }
+                    tnc.close();
                 }
             }
         }
 
     #[error("given callsign is {0} characters long; maximum is 9")]
     CallsignTooLong(usize),
 
-    #[error("error during soundcard initialisation")]
-    SoundcardInit,
-
-    #[error("unable to locate sound card '{0}' - is it in use?")]
-    SoundcardNotFound(String),
-
-    #[error("unable to set up RTL-SDR receiver")]
-    RtlSdrInit,
-
     #[error(
         "provided packet payload is too large: provided {provided} bytes, capacity {capacity}"
     )]
 
 };
 
 use crate::{
-    error::{M17Error, SoundmodemError},
-    soundmodem::{InputSource, SoundmodemEvent},
+    error::M17Error,
+    soundmodem::{InputSource, SoundmodemErrorSender, SoundmodemEvent},
 };
 
 pub struct RtlSdr {
 }
 
 impl InputSource for RtlSdr {
-    fn start(&self, tx: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
-        let mut cmd = Command::new("rtl_fm")
+    fn start(&self, tx: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
+        let mut cmd = match Command::new("rtl_fm")
             .args([
                 "-E",
                 "offset",
                 "48k",
             ])
             .stdout(Stdio::piped())
-            .spawn()?;
+            .spawn()
+        {
+            Ok(c) => c,
+            Err(e) => {
+                errors.send_error(e);
+                return;
+            }
+        };
         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) -> Result<(), SoundmodemError> {
+    fn close(&self) {
         if let Some(mut process) = self.rtlfm.lock().unwrap().take() {
             let _ = process.kill();
         }
-        Ok(())
     }
 }
 
     }
 
     pub fn new(port_name: &str, pin: PttPin) -> Result<Self, SoundmodemError> {
-        // TODO: error handling
-        let port = serialport::new(port_name, 9600).open().unwrap();
+        let port = serialport::new(port_name, 9600).open()?;
         let mut s = Self { port, pin };
         s.ptt_off()?;
         Ok(s)
 
 
 use cpal::{
     traits::{DeviceTrait, HostTrait, StreamTrait},
-    SampleFormat, SampleRate, Stream,
+    BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError,
+    SupportedStreamConfigsError,
 };
+use thiserror::Error;
 
-use crate::{
-    error::{M17Error, SoundmodemError},
-    soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent},
+use crate::soundmodem::{
+    InputSource, OutputBuffer, OutputSink, SoundmodemErrorSender, SoundmodemEvent,
 };
 
 pub struct Soundcard {
 }
 
 impl Soundcard {
-    pub fn new<S: Into<String>>(card_name: S) -> Result<Self, M17Error> {
+    pub fn new<S: Into<String>>(card_name: S) -> Result<Self, SoundcardError> {
         let (card_tx, card_rx) = sync_channel(128);
         let (setup_tx, setup_rx) = sync_channel(1);
         spawn_soundcard_worker(card_rx, setup_tx, card_name.into());
         match setup_rx.recv() {
             Ok(Ok(())) => Ok(Self { event_tx: card_tx }),
             Ok(Err(e)) => Err(e),
-            Err(_) => Err(M17Error::SoundcardInit),
+            Err(_) => Err(SoundcardError::SoundcardInit),
         }
     }
 
     SetTxInverted(bool),
     StartInput {
         samples: SyncSender<SoundmodemEvent>,
+        errors: SoundmodemErrorSender,
     },
     CloseInput,
     StartOutput {
         event_tx: SyncSender<SoundmodemEvent>,
         buffer: Arc<RwLock<OutputBuffer>>,
+        errors: SoundmodemErrorSender,
     },
     CloseOutput,
 }
 }
 
 impl InputSource for SoundcardInputSource {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
-        Ok(self.event_tx.send(SoundcardEvent::StartInput { samples })?)
+    fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
+        let _ = self
+            .event_tx
+            .send(SoundcardEvent::StartInput { samples, errors });
     }
 
-    fn close(&self) -> Result<(), SoundmodemError> {
-        Ok(self.event_tx.send(SoundcardEvent::CloseInput)?)
+    fn close(&self) {
+        let _ = self.event_tx.send(SoundcardEvent::CloseInput);
     }
 }
 
         &self,
         event_tx: SyncSender<SoundmodemEvent>,
         buffer: Arc<RwLock<OutputBuffer>>,
-    ) -> Result<(), SoundmodemError> {
-        Ok(self
-            .event_tx
-            .send(SoundcardEvent::StartOutput { event_tx, buffer })?)
+        errors: SoundmodemErrorSender,
+    ) {
+        let _ = self.event_tx.send(SoundcardEvent::StartOutput {
+            event_tx,
+            buffer,
+            errors,
+        });
     }
 
-    fn close(&self) -> Result<(), SoundmodemError> {
-        Ok(self.event_tx.send(SoundcardEvent::CloseOutput)?)
+    fn close(&self) {
+        let _ = self.event_tx.send(SoundcardEvent::CloseOutput);
     }
 }
 
 fn spawn_soundcard_worker(
     event_rx: Receiver<SoundcardEvent>,
-    setup_tx: SyncSender<Result<(), M17Error>>,
+    setup_tx: SyncSender<Result<(), SoundcardError>>,
     card_name: String,
 ) {
     std::thread::spawn(move || {
             .unwrap()
             .find(|d| d.name().unwrap() == card_name)
         else {
-            let _ = setup_tx.send(Err(M17Error::SoundcardNotFound(card_name)));
+            let _ = setup_tx.send(Err(SoundcardError::CardNotFound(card_name)));
             return;
         };
 
             match ev {
                 SoundcardEvent::SetRxInverted(inv) => rx_inverted = inv,
                 SoundcardEvent::SetTxInverted(inv) => tx_inverted = inv,
-                SoundcardEvent::StartInput { samples } => {
-                    let mut input_configs = device.supported_input_configs().unwrap();
-                    let input_config = input_configs
+                SoundcardEvent::StartInput { samples, errors } => {
+                    let mut input_configs = match device.supported_input_configs() {
+                        Ok(c) => c,
+                        Err(e) => {
+                            errors.send_error(SoundcardError::SupportedConfigs(e));
+                            continue;
+                        }
+                    };
+                    let input_config = match input_configs
                         .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
-                        .unwrap()
-                        .with_sample_rate(SampleRate(48000));
-                    let stream = device
-                        .build_input_stream(
-                            &input_config.into(),
-                            move |data: &[i16], _info: &cpal::InputCallbackInfo| {
-                                let out: Vec<i16> = data
-                                    .iter()
-                                    .map(|s| if rx_inverted { s.saturating_neg() } else { *s })
-                                    .collect();
-                                let _ =
-                                    samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
-                            },
-                            |e| {
-                                // TODO: abort?
-                                log::debug!("error occurred in soundcard input: {e:?}");
-                            },
-                            None,
-                        )
-                        .unwrap();
-                    stream.play().unwrap();
+                    {
+                        Some(c) => c,
+                        None => {
+                            errors.send_error(SoundcardError::NoValidConfigAvailable);
+                            continue;
+                        }
+                    };
+                    let input_config = input_config.with_sample_rate(SampleRate(48000));
+                    let errors_1 = errors.clone();
+                    let stream = match device.build_input_stream(
+                        &input_config.into(),
+                        move |data: &[i16], _info: &cpal::InputCallbackInfo| {
+                            let out: Vec<i16> = data
+                                .iter()
+                                .map(|s| if rx_inverted { s.saturating_neg() } else { *s })
+                                .collect();
+                            let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
+                        },
+                        move |e| {
+                            errors_1.send_error(SoundcardError::Stream(e));
+                        },
+                        None,
+                    ) {
+                        Ok(s) => s,
+                        Err(e) => {
+                            errors.send_error(SoundcardError::StreamBuild(e));
+                            continue;
+                        }
+                    };
+                    if let Err(e) = stream.play() {
+                        errors.send_error(SoundcardError::StreamPlay(e));
+                        continue;
+                    }
                     input_stream = Some(stream);
                 }
                 SoundcardEvent::CloseInput => {
                     let _ = input_stream.take();
                 }
-                SoundcardEvent::StartOutput { event_tx, buffer } => {
-                    let mut output_configs = device.supported_output_configs().unwrap();
-                    // TODO: more error handling
-                    let output_config = output_configs
+                SoundcardEvent::StartOutput {
+                    event_tx,
+                    buffer,
+                    errors,
+                } => {
+                    let mut output_configs = match device.supported_output_configs() {
+                        Ok(c) => c,
+                        Err(e) => {
+                            errors.send_error(SoundcardError::SupportedConfigs(e));
+                            continue;
+                        }
+                    };
+                    let output_config = match output_configs
                         .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
-                        .unwrap()
-                        .with_sample_rate(SampleRate(48000));
-                    let stream = device
-                        .build_output_stream(
-                            &output_config.into(),
-                            move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
-                                let mut taken = 0;
-                                let ts = info.timestamp();
-                                let latency = ts
-                                    .playback
-                                    .duration_since(&ts.callback)
-                                    .unwrap_or(Duration::ZERO);
-                                let mut buffer = buffer.write().unwrap();
-                                buffer.latency = latency;
-                                for out in data.iter_mut() {
-                                    if let Some(s) = buffer.samples.pop_front() {
-                                        *out = if tx_inverted { s.saturating_neg() } else { s };
-                                        taken += 1;
-                                    } else if buffer.idling {
-                                        *out = 0;
-                                    } else {
-                                        log::debug!("output soundcard had underrun");
-                                        let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
-                                        break;
-                                    }
+                    {
+                        Some(c) => c,
+                        None => {
+                            errors.send_error(SoundcardError::NoValidConfigAvailable);
+                            continue;
+                        }
+                    };
+                    let output_config = output_config.with_sample_rate(SampleRate(48000));
+                    let errors_1 = errors.clone();
+                    let stream = match device.build_output_stream(
+                        &output_config.into(),
+                        move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
+                            let mut taken = 0;
+                            let ts = info.timestamp();
+                            let latency = ts
+                                .playback
+                                .duration_since(&ts.callback)
+                                .unwrap_or(Duration::ZERO);
+                            let mut buffer = buffer.write().unwrap();
+                            buffer.latency = latency;
+                            for out in data.iter_mut() {
+                                if let Some(s) = buffer.samples.pop_front() {
+                                    *out = if tx_inverted { s.saturating_neg() } else { s };
+                                    taken += 1;
+                                } else if buffer.idling {
+                                    *out = 0;
+                                } else {
+                                    let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
+                                    break;
                                 }
-                                //debug!("latency is {} ms, taken {taken}", latency.as_millis());
-                                let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
-                                    len: taken,
-                                    timestamp: Instant::now(),
-                                });
-                            },
-                            |e| {
-                                // TODO: abort?
-                                log::debug!("error occurred in soundcard output: {e:?}");
-                            },
-                            None,
-                        )
-                        .unwrap();
-                    stream.play().unwrap();
+                            }
+                            let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
+                                len: taken,
+                                timestamp: Instant::now(),
+                            });
+                        },
+                        move |e| {
+                            errors_1.send_error(SoundcardError::Stream(e));
+                        },
+                        None,
+                    ) {
+                        Ok(s) => s,
+                        Err(e) => {
+                            errors.send_error(SoundcardError::StreamBuild(e));
+                            continue;
+                        }
+                    };
+                    if let Err(e) = stream.play() {
+                        errors.send_error(SoundcardError::StreamPlay(e));
+                        continue;
+                    }
                     output_stream = Some(stream);
                 }
                 SoundcardEvent::CloseOutput => {
         }
     });
 }
+
+#[derive(Debug, Error)]
+pub enum SoundcardError {
+    #[error("sound card init aborted unexpectedly")]
+    SoundcardInit,
+
+    #[error("unable to enumerate devices: {0}")]
+    Host(DevicesError),
+
+    #[error("unable to locate sound card '{0}' - is it in use?")]
+    CardNotFound(String),
+
+    #[error("error occurred in soundcard i/o: {0}")]
+    Stream(#[source] StreamError),
+
+    #[error("unable to retrieve supported configs for soundcard: {0}")]
+    SupportedConfigs(#[source] SupportedStreamConfigsError),
+
+    #[error("could not find a suitable soundcard config")]
+    NoValidConfigAvailable,
+
+    #[error("unable to build soundcard stream: {0}")]
+    StreamBuild(#[source] BuildStreamError),
+
+    #[error("unable to play stream")]
+    StreamPlay(#[source] PlayStreamError),
+}
 
 use crate::error::{M17Error, SoundmodemError};
 use crate::tnc::{Tnc, TncError};
-use log::debug;
 use m17core::kiss::MAX_FRAME_LEN;
 use m17core::modem::{Demodulator, Modulator, ModulatorAction, SoftDemodulator, SoftModulator};
 use m17core::tnc::SoftTnc;
 use std::sync::RwLock;
 use std::sync::{Arc, Mutex};
 use std::time::{Duration, Instant};
+use thiserror::Error;
 
 pub struct Soundmodem {
     event_tx: SyncSender<SoundmodemEvent>,
     kiss_out_rx: Arc<Mutex<Receiver<Arc<[u8]>>>>,
     partial_kiss_out: Arc<Mutex<Option<PartialKissOut>>>,
+    error_handler: ErrorHandlerInternal,
 }
 
 impl Soundmodem {
-    pub fn new<I: InputSource, O: OutputSink, P: Ptt>(input: I, output: O, ptt: P) -> Self {
-        // must create TNC here
+    pub fn new<I: InputSource, O: OutputSink, P: Ptt, E: ErrorHandler>(
+        input: I,
+        output: O,
+        ptt: P,
+        error: E,
+    ) -> Self {
         let (event_tx, event_rx) = sync_channel(128);
         let (kiss_out_tx, kiss_out_rx) = sync_channel(128);
+        let runtime_error_handler: ErrorHandlerInternal = Arc::new(Mutex::new(Box::new(error)));
         spawn_soundmodem_worker(
             event_tx.clone(),
             event_rx,
             Box::new(input),
             Box::new(output),
             Box::new(ptt),
+            runtime_error_handler.clone(),
         );
         Self {
             event_tx,
             kiss_out_rx: Arc::new(Mutex::new(kiss_out_rx)),
             partial_kiss_out: Arc::new(Mutex::new(None)),
+            error_handler: runtime_error_handler,
         }
     }
 }
 
+#[derive(Debug, Clone, Copy)]
+pub enum ErrorSource {
+    Input,
+    Output,
+    Ptt,
+}
+
+pub trait ErrorHandler: Send + Sync + 'static {
+    fn soundmodem_error(&mut self, source: ErrorSource, err: SoundmodemError);
+}
+
+impl<F> ErrorHandler for F
+where
+    F: FnMut(ErrorSource, SoundmodemError) + Send + Sync + 'static,
+{
+    fn soundmodem_error(&mut self, source: ErrorSource, err: SoundmodemError) {
+        self(source, err)
+    }
+}
+
+pub struct NullErrorHandler;
+
+impl NullErrorHandler {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl Default for NullErrorHandler {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl ErrorHandler for NullErrorHandler {
+    fn soundmodem_error(&mut self, source: ErrorSource, err: SoundmodemError) {
+        let _ = source;
+        let _ = err;
+    }
+}
+
+type ErrorHandlerInternal = Arc<Mutex<Box<dyn ErrorHandler>>>;
+
+#[derive(Clone)]
+pub struct SoundmodemErrorSender {
+    source: ErrorSource,
+    event_tx: SyncSender<SoundmodemEvent>,
+}
+
+impl SoundmodemErrorSender {
+    pub fn send_error<E: Into<SoundmodemError>>(&self, err: E) {
+        let _ = self
+            .event_tx
+            .send(SoundmodemEvent::RuntimeError(self.source, err.into()));
+    }
+}
+
 struct PartialKissOut {
     output: Arc<[u8]>,
     idx: usize,
             event_tx: self.event_tx.clone(),
             kiss_out_rx: self.kiss_out_rx.clone(),
             partial_kiss_out: self.partial_kiss_out.clone(),
+            error_handler: self.error_handler.clone(),
         })
     }
 
-    fn start(&mut self) -> Result<(), TncError> {
+    fn start(&mut self) {
         let _ = self.event_tx.send(SoundmodemEvent::Start);
-        Ok(())
     }
 
-    fn close(&mut self) -> Result<(), TncError> {
+    fn close(&mut self) {
         let _ = self.event_tx.send(SoundmodemEvent::Close);
-        Ok(())
     }
 }
 
     Close,
     DidReadFromOutputBuffer { len: usize, timestamp: Instant },
     OutputUnderrun,
+    RuntimeError(ErrorSource, SoundmodemError),
 }
 
 fn spawn_soundmodem_worker(
     input: Box<dyn InputSource>,
     output: Box<dyn OutputSink>,
     mut ptt_driver: Box<dyn Ptt>,
+    error_handler: ErrorHandlerInternal,
 ) {
     std::thread::spawn(move || {
         // TODO: should be able to provide a custom Demodulator for a soundmodem
                     tnc.set_data_carrier_detect(demodulator.data_carrier_detect());
                 }
                 SoundmodemEvent::Start => {
-                    // TODO: runtime event handling
-                    input.start(event_tx.clone()).unwrap();
-                    output.start(event_tx.clone(), out_buffer.clone()).unwrap();
+                    let input_errors = SoundmodemErrorSender {
+                        source: ErrorSource::Input,
+                        event_tx: event_tx.clone(),
+                    };
+                    input.start(event_tx.clone(), input_errors);
+                    let output_errors = SoundmodemErrorSender {
+                        source: ErrorSource::Output,
+                        event_tx: event_tx.clone(),
+                    };
+                    output.start(event_tx.clone(), out_buffer.clone(), output_errors);
                 }
                 SoundmodemEvent::Close => {
-                    ptt_driver.ptt_off().unwrap();
+                    input.close();
+                    output.close();
+                    if let Err(e) = ptt_driver.ptt_off() {
+                        error_handler
+                            .lock()
+                            .unwrap()
+                            .soundmodem_error(ErrorSource::Ptt, e);
+                    }
                     break;
                 }
                 SoundmodemEvent::DidReadFromOutputBuffer { len, timestamp } => {
                     );
                 }
                 SoundmodemEvent::OutputUnderrun => {
+                    log::debug!("output underrun");
                     // TODO: cancel transmission, send empty data frame to host
                 }
+                SoundmodemEvent::RuntimeError(source, err) => {
+                    error_handler.lock().unwrap().soundmodem_error(source, err);
+                }
             }
 
             // Update PTT state
             let new_ptt = tnc.ptt();
             if new_ptt != ptt {
                 if new_ptt {
-                    ptt_driver.ptt_on().unwrap();
-                } else {
-                    ptt_driver.ptt_off().unwrap();
+                    if let Err(e) = ptt_driver.ptt_on() {
+                        error_handler
+                            .lock()
+                            .unwrap()
+                            .soundmodem_error(ErrorSource::Ptt, e);
+                    }
+                } else if let Err(e) = ptt_driver.ptt_off() {
+                    error_handler
+                        .lock()
+                        .unwrap()
+                        .soundmodem_error(ErrorSource::Ptt, e);
                 }
             }
             ptt = new_ptt;
 }
 
 pub trait InputSource: Send + Sync + 'static {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError>;
-    fn close(&self) -> Result<(), SoundmodemError>;
+    fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender);
+    fn close(&self);
 }
 
 pub struct InputRrcFile {
 }
 
 impl InputSource for InputRrcFile {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
+    fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
         let (end_tx, end_rx) = channel();
         let baseband = self.baseband.clone();
         std::thread::spawn(move || {
                 buf[idx] = sample;
                 idx += 1;
                 if idx == SAMPLES_PER_TICK {
-                    if let Err(e) = samples.try_send(SoundmodemEvent::BasebandInput(buf.into())) {
-                        debug!("overflow feeding soundmodem: {e:?}");
+                    if samples
+                        .try_send(SoundmodemEvent::BasebandInput(buf.into()))
+                        .is_err()
+                    {
+                        errors.send_error(InputRrcError::Overflow);
                     }
                     next_tick += TICK;
                     idx = 0;
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
-        Ok(())
     }
 
-    fn close(&self) -> Result<(), SoundmodemError> {
+    fn close(&self) {
         let _ = self.end_tx.lock().unwrap().take();
-        Ok(())
     }
 }
 
+#[derive(Debug, Error)]
+pub enum InputRrcError {
+    #[error("overflow occurred feeding sample to soundmodem")]
+    Overflow,
+}
+
 pub struct NullInputSource {
     end_tx: Mutex<Option<Sender<()>>>,
 }
 }
 
 impl InputSource for NullInputSource {
-    fn start(&self, samples: SyncSender<SoundmodemEvent>) -> Result<(), SoundmodemError> {
+    fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
         let (end_tx, end_rx) = channel();
         std::thread::spawn(move || {
             // assuming 48 kHz for now
                 if end_rx.try_recv() != Err(TryRecvError::Empty) {
                     break;
                 }
-                if let Err(e) = samples.try_send(SoundmodemEvent::BasebandInput(
-                    [0i16; SAMPLES_PER_TICK].into(),
-                )) {
-                    debug!("overflow feeding soundmodem: {e:?}");
+                if samples
+                    .try_send(SoundmodemEvent::BasebandInput(
+                        [0i16; SAMPLES_PER_TICK].into(),
+                    ))
+                    .is_err()
+                {
+                    errors.send_error(NullInputError::Overflow);
                 }
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
-        Ok(())
     }
 
-    fn close(&self) -> Result<(), SoundmodemError> {
+    fn close(&self) {
         let _ = self.end_tx.lock().unwrap().take();
-        Ok(())
     }
 }
 
+#[derive(Debug, Error)]
+pub enum NullInputError {
+    #[error("overflow occurred feeding sample to soundmodem")]
+    Overflow,
+}
+
 impl Default for NullInputSource {
     fn default() -> Self {
         Self::new()
         &self,
         event_tx: SyncSender<SoundmodemEvent>,
         buffer: Arc<RwLock<OutputBuffer>>,
-    ) -> Result<(), SoundmodemError>;
-    fn close(&self) -> Result<(), SoundmodemError>;
+        errors: SoundmodemErrorSender,
+    );
+    fn close(&self);
 }
 
 pub struct OutputRrcFile {
         &self,
         event_tx: SyncSender<SoundmodemEvent>,
         buffer: Arc<RwLock<OutputBuffer>>,
-    ) -> Result<(), SoundmodemError> {
+        errors: SoundmodemErrorSender,
+    ) {
         let (end_tx, end_rx) = channel();
-        let mut file = File::create(self.path.clone())?;
+        let mut file = match File::create(self.path.clone()) {
+            Ok(f) => f,
+            Err(e) => {
+                errors.send_error(OutputRrcError::Open(e));
+                return;
+            }
+        };
         std::thread::spawn(move || {
             // assuming 48 kHz for now
             const TICK: Duration = Duration::from_millis(25);
                         out.copy_from_slice(&[be[0], be[1]]);
                         buf_used += 2;
                     } else if !buffer.idling {
-                        debug!("output rrc file had underrun");
                         let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
                         break;
                     }
                 }
                 if let Err(e) = file.write_all(&buf[0..buf_used]) {
-                    debug!("failed to write to rrc file: {e:?}");
+                    errors.send_error(OutputRrcError::WriteError(e));
                     break;
                 }
                 let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
-        Ok(())
     }
 
-    fn close(&self) -> Result<(), SoundmodemError> {
+    fn close(&self) {
         let _ = self.end_tx.lock().unwrap().take();
-        Ok(())
     }
 }
 
+#[derive(Debug, Error)]
+pub enum OutputRrcError {
+    #[error("unable to open rrc file for writing: {0}")]
+    Open(#[source] std::io::Error),
+
+    #[error("error writing to output file: {0}")]
+    WriteError(#[source] std::io::Error),
+}
+
 pub struct NullOutputSink {
     end_tx: Mutex<Option<Sender<()>>>,
 }
         &self,
         event_tx: SyncSender<SoundmodemEvent>,
         buffer: Arc<RwLock<OutputBuffer>>,
-    ) -> Result<(), SoundmodemError> {
+        _errors: SoundmodemErrorSender,
+    ) {
         let (end_tx, end_rx) = channel();
         std::thread::spawn(move || {
             // assuming 48 kHz for now
                 for _ in 0..SAMPLES_PER_TICK {
                     if buffer.samples.pop_front().is_none() {
                         if !buffer.idling {
-                            debug!("null output had underrun");
                             let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
                             break;
                         }
             }
         });
         *self.end_tx.lock().unwrap() = Some(end_tx);
-        Ok(())
     }
 
-    fn close(&self) -> Result<(), SoundmodemError> {
+    fn close(&self) {
         let _ = self.end_tx.lock().unwrap().take();
-        Ok(())
     }
 }
 
 
         Ok(self.clone())
     }
 
-    fn start(&mut self) -> Result<(), crate::tnc::TncError> {
-        Ok(())
-    }
+    fn start(&mut self) {}
 
-    fn close(&mut self) -> Result<(), crate::tnc::TncError> {
-        Ok(())
-    }
+    fn close(&mut self) {}
 }
 
 impl Write for NullTnc {
 
     fn try_clone(&mut self) -> Result<Self, TncError>;
 
     /// Start I/O.
-    fn start(&mut self) -> Result<(), TncError>;
+    fn start(&mut self);
 
     /// Shut down I/O - it is assumed we cannot restart.
-    fn close(&mut self) -> Result<(), TncError>;
+    fn close(&mut self);
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
         std::net::TcpStream::try_clone(self).map_err(|_| TncError::Unknown)
     }
 
-    fn start(&mut self) -> Result<(), TncError> {
+    fn start(&mut self) {
         // already started, hopefully we get onto reading the socket quickly
-        Ok(())
     }
 
-    fn close(&mut self) -> Result<(), TncError> {
-        self.shutdown(std::net::Shutdown::Both)
-            .map_err(|_| TncError::Unknown)
+    fn close(&mut self) {
+        let _ = self.shutdown(std::net::Shutdown::Both);
     }
 }
 
 use m17app::app::M17App;
 use m17app::soundcard::Soundcard;
-use m17app::soundmodem::{NullOutputSink, NullPtt, Soundmodem};
+use m17app::soundmodem::{NullErrorHandler, NullOutputSink, NullPtt, Soundmodem};
 use m17codec2::Codec2Adapter;
 
 pub fn demod_test() {
     let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
-    let soundmodem = Soundmodem::new(soundcard.input(), NullOutputSink::new(), NullPtt::new());
+    let soundmodem = Soundmodem::new(
+        soundcard.input(),
+        NullOutputSink::new(),
+        NullPtt::new(),
+        NullErrorHandler::new(),
+    );
     let app = M17App::new(soundmodem);
     app.add_stream_adapter(Codec2Adapter::new()).unwrap();
     app.start().unwrap();
 
 use m17app::link_setup::M17Address;
 use m17app::serial::{PttPin, SerialPtt};
 use m17app::soundcard::Soundcard;
-use m17app::soundmodem::Soundmodem;
+use m17app::soundmodem::{NullErrorHandler, Soundmodem};
 use m17codec2::WavePlayer;
 use std::path::PathBuf;
 
     let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
     soundcard.set_tx_inverted(true);
     let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts).unwrap();
-    let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt);
+    let soundmodem = Soundmodem::new(
+        soundcard.input(),
+        soundcard.output(),
+        ptt,
+        NullErrorHandler::new(),
+    );
     let app = M17App::new(soundmodem);
     app.start().unwrap();
     std::thread::sleep(std::time::Duration::from_secs(1));
 
 use m17app::app::M17App;
 use m17app::link_setup::LinkSetup;
 use m17app::soundcard::Soundcard;
-use m17app::soundmodem::{NullOutputSink, NullPtt, Soundmodem};
+use m17app::soundmodem::{NullErrorHandler, NullOutputSink, NullPtt, Soundmodem};
 use m17app::PacketType;
 use std::sync::Arc;
 
 fn main() {
     let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
-    let soundmodem = Soundmodem::new(soundcard.input(), NullOutputSink::new(), NullPtt::new());
+    let soundmodem = Soundmodem::new(
+        soundcard.input(),
+        NullOutputSink::new(),
+        NullPtt::new(),
+        NullErrorHandler::new(),
+    );
     let app = M17App::new(soundmodem);
     app.add_packet_adapter(PacketPrinter).unwrap();
     app.start().unwrap();
 
 use m17app::link_setup::{LinkSetup, M17Address};
 use m17app::serial::{PttPin, SerialPtt};
 use m17app::soundcard::Soundcard;
-use m17app::soundmodem::Soundmodem;
+use m17app::soundmodem::{NullErrorHandler, Soundmodem};
 use m17core::protocol::PacketType;
 
 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).unwrap();
-    let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt);
+    let soundmodem = Soundmodem::new(
+        soundcard.input(),
+        soundcard.output(),
+        ptt,
+        NullErrorHandler::new(),
+    );
     let app = M17App::new(soundmodem);
 
     app.start().unwrap();