diff --git a/async/Cargo.toml b/async/Cargo.toml
index 5ab08b296bc35f4324db95a74b09f345b110ca07..0f2227a5b0385c7d80139f4efb0440ef8c9b1039 100644
--- a/async/Cargo.toml
+++ b/async/Cargo.toml
@@ -19,7 +19,8 @@ argh = "0.1"
 # pin-project = "1.0"
 # parking_lot = { version = "0.12", features = ["arc_lock", "send_guard"] }
 
-tokio = { version = "1.17", features = ["sync"] }
+# "net" for AsyncFd on unix
+tokio = { version = "1.17", features = ["sync", "net"] }
 # require alpha for https://github.com/rust-lang/futures-rs/pull/2571
 futures = { version = "0.4.0-alpha.0", git = "https://github.com/rust-lang/futures-rs", revision = "8b0f812f53ada0d0aeb74abc32be22ab9dafae05" }
 async-trait = "0.1"
diff --git a/async/examples/con1.rs b/async/examples/con1.rs
index e7d36fda249db1a9c942a4c281f7b90e38db4b38..0b42db6416189e0bc408f61154de2ac491ed72ac 100644
--- a/async/examples/con1.rs
+++ b/async/examples/con1.rs
@@ -4,6 +4,7 @@ use {
     log::{debug, error, info, log, trace, warn},
 };
 use anyhow::{Context, Result, Error, bail};
+// use snafu::ResultExt;
 use pretty_hex::PrettyHex;
 
 use tokio::net::TcpStream;
@@ -164,9 +165,9 @@ async fn run(args: &Args) -> Result<()> {
                     let (mut io, mut err) = r;
                     tokio::spawn(async move {
                         trace!("channel copy");
-                        let mut i = door_async::stdin().unwrap();
+                        let mut i = door_async::stdin()?;
                         // let mut o = tokio::io::stdout();
-                        let mut o = door_async::stdout().unwrap();
+                        let mut o = door_async::stdout()?;
                         let mut e = tokio::io::stderr();
                         let mut io2 = io.clone();
                         let co = tokio::io::copy(&mut io, &mut o);
@@ -178,11 +179,13 @@ async fn run(args: &Args) -> Result<()> {
                         r3?;
                         Ok::<_, anyhow::Error>(())
                     });
+                    // TODO: handle channel completion
                 }
                 Some(_) => unreachable!(),
                 None => {},
             }
         }
+        #[allow(unreachable_code)]
         Ok::<_, anyhow::Error>(())
     });
 
diff --git a/async/src/fdio.rs b/async/src/fdio.rs
index deb99a1dffd14cc3b36bb065070b610fab73bc39..00f05515c99f5b691566565de0db53b4f6f378ce 100644
--- a/async/src/fdio.rs
+++ b/async/src/fdio.rs
@@ -5,8 +5,7 @@ use snafu::{prelude::*, Whatever};
 
 use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
 use tokio::io::unix::AsyncFd;
-use std::os::unix::io::{RawFd, FromRawFd};
-use std::io::{Read, Write};
+use std::os::unix::io::RawFd;
 
 use std::io::Error as IoError;
 
@@ -14,12 +13,11 @@ use core::pin::Pin;
 use core::task::{Context, Poll};
 
 use nix::fcntl::{fcntl, FcntlArg, OFlag};
-use nix::errno::Errno;
 
-fn dup_async(orig_fd: libc::c_int) -> Result<AsyncFd<RawFd>, Whatever> {
-    let fd = nix::unistd::dup(orig_fd).whatever_context("dup() failed")?;
-    fcntl(fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).whatever_context("fcntl failed")?;
-    AsyncFd::new(fd).whatever_context("asyncfd")
+fn dup_async(orig_fd: libc::c_int) -> Result<AsyncFd<RawFd>, IoError> {
+    let fd = nix::unistd::dup(orig_fd)?;
+    fcntl(fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?;
+    AsyncFd::new(fd)
 }
 
 pub struct Stdin {
@@ -32,17 +30,17 @@ pub struct Stderr {
     f: AsyncFd<RawFd>,
 }
 
-pub fn stdin() -> Result<Stdin, Whatever> {
+pub fn stdin() -> Result<Stdin, IoError> {
     Ok(Stdin {
         f: dup_async(libc::STDIN_FILENO)?,
     })
 }
-pub fn stdout() -> Result<Stdout, Whatever> {
+pub fn stdout() -> Result<Stdout, IoError> {
     Ok(Stdout {
         f: dup_async(libc::STDOUT_FILENO)?,
     })
 }
-pub fn stderr() -> Result<Stderr, Whatever> {
+pub fn stderr() -> Result<Stderr, IoError> {
     Ok(Stderr {
         f: dup_async(libc::STDERR_FILENO)?,
     })
@@ -72,6 +70,7 @@ impl AsyncRead for Stdin {
         cx: &mut Context<'_>,
         buf: &mut ReadBuf,
     ) -> Poll<Result<(), IoError>> {
+        // XXX loop was copy pasted from docs, perhaps it could be simpler
         loop {
             let mut guard = match self.f.poll_read_ready(cx)? {
                 Poll::Ready(r) => r,
diff --git a/async/src/lib.rs b/async/src/lib.rs
index 954d5141421373176c9ec13ac2c8a6821a5a6d2b..8773434c2a445c705d0297d477dc07c8c91e5a0c 100644
--- a/async/src/lib.rs
+++ b/async/src/lib.rs
@@ -1,11 +1,15 @@
+#![forbid(unsafe_code)]
+#![allow(unused_imports)]
 
 mod client;
 mod async_door;
 mod simple_client;
-mod fdio;
 
 pub use async_door::AsyncDoor;
 pub use client::SSHClient;
-pub use fdio::{stdin, stdout, stderr};
 pub use simple_client::SimpleClient;
 
+#[cfg(unix)]
+mod fdio;
+#[cfg(unix)]
+pub use fdio::{stdin, stdout, stderr};