diff --git a/actix-tls/CHANGES.md b/actix-tls/CHANGES.md
index 02db1c55..1ca18462 100644
--- a/actix-tls/CHANGES.md
+++ b/actix-tls/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+- Implement `Error` for `TlsError` when all variants' inner errors also impl `Error`. [#438]
+
+[#438]: https://github.com/actix/actix-net/pull/438
 
 
 ## 3.0.1 - 2022-01-11
diff --git a/actix-tls/src/accept/mod.rs b/actix-tls/src/accept/mod.rs
index 0f387e0c..f31665ec 100644
--- a/actix-tls/src/accept/mod.rs
+++ b/actix-tls/src/accept/mod.rs
@@ -2,10 +2,12 @@
 
 use std::{
     convert::Infallible,
+    error::Error,
+    fmt,
     sync::atomic::{AtomicUsize, Ordering},
 };
 
-use actix_utils::{counter::Counter, derive};
+use actix_utils::counter::Counter;
 
 #[cfg(feature = "openssl")]
 #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
@@ -42,23 +44,42 @@ pub fn max_concurrent_tls_connect(num: usize) {
 /// TLS handshake error, TLS timeout, or inner service error.
 ///
 /// All TLS acceptors from this crate will return the `SvcErr` type parameter as [`Infallible`],
-/// which can be cast to your own service type, inferred or otherwise,
-/// using [`into_service_error`](Self::into_service_error).
+/// which can be cast to your own service type, inferred or otherwise, using [`into_service_error`].
+///
+/// [`into_service_error`]: Self::into_service_error
 #[derive(Debug)]
 pub enum TlsError<TlsErr, SvcErr> {
     /// TLS handshake has timed-out.
     Timeout,
+
     /// Wraps TLS service errors.
     Tls(TlsErr),
+
     /// Wraps service errors.
     Service(SvcErr),
 }
 
-derive::enum_error! {
-    match TlsError<TlsErr, SvcErr> |f| {
-        Timeout => "TLS handshake has timed-out",
-        Tls(e) => "TLS handshake error",
-        Service(e) => "Service error",
+impl<TlsErr, SvcErr> fmt::Display for TlsError<TlsErr, SvcErr> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Timeout => f.write_str("TLS handshake has timed-out"),
+            Self::Tls(_) => f.write_str("TLS handshake error"),
+            Self::Service(_) => f.write_str("Service error"),
+        }
+    }
+}
+
+impl<TlsErr, SvcErr> Error for TlsError<TlsErr, SvcErr>
+where
+    TlsErr: 'static + Error,
+    SvcErr: 'static + Error,
+{
+    fn source(&self) -> Option<&(dyn Error + 'static)> {
+        match self {
+            TlsError::Timeout => todo!(),
+            TlsError::Tls(err) => Some(err),
+            TlsError::Service(err) => Some(err),
+        }
     }
 }
 
diff --git a/actix-tls/src/accept/native_tls.rs b/actix-tls/src/accept/native_tls.rs
index c853c77f..a4c6077e 100644
--- a/actix-tls/src/accept/native_tls.rs
+++ b/actix-tls/src/accept/native_tls.rs
@@ -10,6 +10,7 @@ use std::{
     time::Duration,
 };
 
+use crate::impl_more;
 use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
 use actix_rt::{
     net::{ActixStream, Ready},
@@ -18,7 +19,6 @@ use actix_rt::{
 use actix_service::{Service, ServiceFactory};
 use actix_utils::{
     counter::Counter,
-    derive,
     future::{ready, Ready as FutReady},
 };
 use futures_core::future::LocalBoxFuture;
@@ -35,9 +35,9 @@ pub mod reexports {
 /// Wraps a `native-tls` based async TLS stream in order to implement [`ActixStream`].
 pub struct TlsStream<IO>(tokio_native_tls::TlsStream<IO>);
 
-derive::from! { tokio_native_tls::TlsStream<IO> => TlsStream<IO> }
-derive::deref! { TlsStream<IO> => 0: tokio_native_tls::TlsStream<IO> }
-derive::deref_mut! { TlsStream<IO> => 0 }
+impl_more::from! { tokio_native_tls::TlsStream<IO> => TlsStream<IO> }
+impl_more::deref! { TlsStream<IO> => 0: tokio_native_tls::TlsStream<IO> }
+impl_more::deref_mut! { TlsStream<IO> => 0 }
 
 impl<IO: ActixStream> AsyncRead for TlsStream<IO> {
     fn poll_read(
diff --git a/actix-tls/src/accept/openssl.rs b/actix-tls/src/accept/openssl.rs
index 47e7bdd8..6e65e5fc 100644
--- a/actix-tls/src/accept/openssl.rs
+++ b/actix-tls/src/accept/openssl.rs
@@ -19,13 +19,13 @@ use actix_rt::{
 use actix_service::{Service, ServiceFactory};
 use actix_utils::{
     counter::{Counter, CounterGuard},
-    derive,
     future::{ready, Ready as FutReady},
 };
 use openssl::ssl::{Error, Ssl, SslAcceptor};
 use pin_project_lite::pin_project;
 
 use super::{TlsError, DEFAULT_TLS_HANDSHAKE_TIMEOUT, MAX_CONN_COUNTER};
+use crate::impl_more;
 
 pub mod reexports {
     //! Re-exports from `openssl` that are useful for acceptors.
@@ -38,9 +38,9 @@ pub mod reexports {
 /// Wraps an `openssl` based async TLS stream in order to implement [`ActixStream`].
 pub struct TlsStream<IO>(tokio_openssl::SslStream<IO>);
 
-derive::from! { tokio_openssl::SslStream<IO> => TlsStream<IO> }
-derive::deref! { TlsStream<IO> => 0: tokio_openssl::SslStream<IO> }
-derive::deref_mut! { TlsStream<IO> => 0 }
+impl_more::from! { tokio_openssl::SslStream<IO> => TlsStream<IO> }
+impl_more::deref! { TlsStream<IO> => 0: tokio_openssl::SslStream<IO> }
+impl_more::deref_mut! { TlsStream<IO> => 0 }
 
 impl<IO: ActixStream> AsyncRead for TlsStream<IO> {
     fn poll_read(
diff --git a/actix-tls/src/accept/rustls.rs b/actix-tls/src/accept/rustls.rs
index 0742b97d..35fbd34a 100644
--- a/actix-tls/src/accept/rustls.rs
+++ b/actix-tls/src/accept/rustls.rs
@@ -20,7 +20,6 @@ use actix_rt::{
 use actix_service::{Service, ServiceFactory};
 use actix_utils::{
     counter::{Counter, CounterGuard},
-    derive,
     future::{ready, Ready as FutReady},
 };
 use pin_project_lite::pin_project;
@@ -28,6 +27,7 @@ use tokio_rustls::rustls::ServerConfig;
 use tokio_rustls::{Accept, TlsAcceptor};
 
 use super::{TlsError, DEFAULT_TLS_HANDSHAKE_TIMEOUT, MAX_CONN_COUNTER};
+use crate::impl_more;
 
 pub mod reexports {
     //! Re-exports from `rustls` that are useful for acceptors.
@@ -38,9 +38,9 @@ pub mod reexports {
 /// Wraps a `rustls` based async TLS stream in order to implement [`ActixStream`].
 pub struct TlsStream<IO>(tokio_rustls::server::TlsStream<IO>);
 
-derive::from! { tokio_rustls::server::TlsStream<IO> => TlsStream<IO> }
-derive::deref! { TlsStream<IO> => 0: tokio_rustls::server::TlsStream<IO> }
-derive::deref_mut! { TlsStream<IO> => 0 }
+impl_more::from! { tokio_rustls::server::TlsStream<IO> => TlsStream<IO> }
+impl_more::deref! { TlsStream<IO> => 0: tokio_rustls::server::TlsStream<IO> }
+impl_more::deref_mut! { TlsStream<IO> => 0 }
 
 impl<IO: ActixStream> AsyncRead for TlsStream<IO> {
     fn poll_read(
diff --git a/actix-tls/src/connect/connection.rs b/actix-tls/src/connect/connection.rs
index 41215597..14c8dc00 100644
--- a/actix-tls/src/connect/connection.rs
+++ b/actix-tls/src/connect/connection.rs
@@ -1,5 +1,5 @@
 use super::Host;
-use actix_utils::derive;
+use crate::impl_more;
 
 /// Wraps underlying I/O and the connection request that initiated it.
 #[derive(Debug)]
@@ -8,8 +8,8 @@ pub struct Connection<R, IO> {
     pub(crate) io: IO,
 }
 
-derive::deref! { Connection<R, IO> => io: IO }
-derive::deref_mut! { Connection<R, IO> => io }
+impl_more::deref! { Connection<R, IO> => io: IO }
+impl_more::deref_mut! { Connection<R, IO> => io }
 
 impl<R, IO> Connection<R, IO> {
     /// Construct new `Connection` from request and IO parts.
diff --git a/actix-tls/src/connect/error.rs b/actix-tls/src/connect/error.rs
index bba9a965..3f89c3e6 100644
--- a/actix-tls/src/connect/error.rs
+++ b/actix-tls/src/connect/error.rs
@@ -1,27 +1,44 @@
-use actix_utils::derive;
-use std::io;
+use std::{error::Error, fmt, io};
 
 /// Errors that can result from using a connector service.
 #[derive(Debug)]
 pub enum ConnectError {
-    /// Failed to resolve the hostname
+    /// Failed to resolve the hostname.
     Resolver(Box<dyn std::error::Error>),
-    /// No DNS records
+
+    /// No DNS records.
     NoRecords,
-    /// Invalid input
+
+    /// Invalid input.
     InvalidInput,
-    /// Unresolved host name
+
+    /// Unresolved host name.
     Unresolved,
-    /// Connection IO error
+
+    /// Connection IO error.
     Io(io::Error),
 }
 
-derive::enum_error! {
-    match ConnectError |f| {
-        NoRecords => "No DNS records found for the input",
-        InvalidInput => "Invalid input",
-        Unresolved => "Connector received `Connect` method with unresolved host",
-        #[source(&**e)] Resolver(e) => "Failed to resolve hostname",
-        #[source(e)] Io(e) => return write!(f, "{}", e),
+impl fmt::Display for ConnectError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::NoRecords => f.write_str("No DNS records found for the input"),
+            Self::InvalidInput => f.write_str("Invalid input"),
+            Self::Unresolved => {
+                f.write_str("Connector received `Connect` method with unresolved host")
+            }
+            Self::Resolver(_) => f.write_str("Failed to resolve hostname"),
+            Self::Io(_) => f.write_str("I/O error"),
+        }
+    }
+}
+
+impl Error for ConnectError {
+    fn source(&self) -> Option<&(dyn Error + 'static)> {
+        match self {
+            ConnectError::Resolver(err) => Some(&**err),
+            ConnectError::Io(err) => Some(err),
+            _ => None,
+        }
     }
 }
diff --git a/actix-tls/src/impl_more.rs b/actix-tls/src/impl_more.rs
new file mode 100644
index 00000000..c380228b
--- /dev/null
+++ b/actix-tls/src/impl_more.rs
@@ -0,0 +1,40 @@
+/// A helper to implement `Deref` for a type.
+#[macro_export]
+macro_rules! deref {
+    ($ty:ident $(<$($generic:ident),*>)? => $field:tt: $target:ty) => {
+        impl $(<$($generic),*>)? ::core::ops::Deref for $ty $(<$($generic),*>)? {
+            type Target = $target;
+
+            fn deref(&self) -> &Self::Target {
+                &self.$field
+            }
+        }
+    };
+}
+
+/// A helper to implement `DerefMut` for a type.
+#[macro_export]
+macro_rules! deref_mut {
+    ($ty:ident $(<$($generic:ident),*>)? => $field:tt) => {
+        impl $(<$($generic),*>)? ::core::ops::DerefMut for $ty $(<$($generic),*>)? {
+            fn deref_mut(&mut self) -> &mut Self::Target {
+                &mut self.$field
+            }
+        }
+    };
+}
+
+/// A helper to implement `From` for a unit struct.
+#[macro_export]
+macro_rules! from {
+    ($from:ty => $ty:ident $(<$($generic:ident),*>)?) => {
+        impl $(<$($generic),*>)? ::core::convert::From<$from> for $ty $(<$($generic),*>)? {
+            fn from(from: $from) -> Self {
+                Self(from)
+            }
+        }
+    };
+}
+
+#[allow(unused_imports)]
+pub(crate) use crate::{deref, deref_mut, from};
diff --git a/actix-tls/src/lib.rs b/actix-tls/src/lib.rs
index 39714dca..dfca00cd 100644
--- a/actix-tls/src/lib.rs
+++ b/actix-tls/src/lib.rs
@@ -18,3 +18,5 @@ pub mod accept;
 #[cfg(feature = "connect")]
 #[cfg_attr(docsrs, doc(cfg(feature = "connect")))]
 pub mod connect;
+
+mod impl_more;
diff --git a/actix-utils/src/derive.rs b/actix-utils/src/derive.rs
deleted file mode 100644
index 499b3db1..00000000
--- a/actix-utils/src/derive.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-/// A helper to implement `Deref` for a type.
-#[doc(hidden)]
-#[macro_export]
-macro_rules! deref {
-    ($ty:ident $(<$($generic:ident),*>)? => $field:tt: $target:ty) => {
-        impl $(<$($generic),*>)? ::std::ops::Deref for $ty $(<$($generic),*>)? {
-            type Target = $target;
-
-            fn deref(&self) -> &Self::Target {
-                &self.$field
-            }
-        }
-    };
-}
-
-/// A helper to implement `DerefMut` for a type.
-#[doc(hidden)]
-#[macro_export]
-macro_rules! deref_mut {
-    ($ty:ident $(<$($generic:ident),*>)? => $field:tt) => {
-        impl $(<$($generic),*>)? ::std::ops::DerefMut for $ty $(<$($generic),*>)? {
-            fn deref_mut(&mut self) -> &mut Self::Target {
-                &mut self.$field
-            }
-        }
-    };
-}
-
-/// A helper to implement `From` for a unit struct.
-#[doc(hidden)]
-#[macro_export]
-macro_rules! from {
-    ($from:ty => $ty:ident $(<$($generic:ident),*>)?) => {
-        impl $(<$($generic),*>)? ::std::convert::From<$from> for $ty $(<$($generic),*>)? {
-            fn from(from: $from) -> Self {
-                Self(from)
-            }
-        }
-    };
-}
-
-/// A helper to implement `Display` and `Error` for an enum.
-#[doc(hidden)]
-#[macro_export]
-macro_rules! enum_error {
-    (match $ty:ident $(<$($generic:ident),*>)? |$f:ident| {$(
-        $( #[source($source:expr)] )? $variant:pat => $fmt:expr $(,)?
-    ),*}) => {
-        #[allow(unused)]
-        impl $(<$($generic),*>)? ::std::fmt::Display for $ty $(<$($generic),*>)? {
-            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
-                use $ty::*;
-
-                let $f = f;
-                let res = match self {$(
-                    $variant => $fmt,
-                )*};
-
-                write!($f, "{}", res)
-            }
-        }
-
-        impl $(<$($generic: ::std::fmt::Debug),*>)? ::std::error::Error for $ty $(<$($generic),*>)? {
-            fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
-                use $ty::*;
-
-                match self {
-                    $($( $variant => { Some($source) } )?)*
-                    _ => None
-                }
-            }
-        }
-    };
-}
-
-#[doc(inline)]
-pub use crate::{deref, deref_mut, enum_error, from};
diff --git a/actix-utils/src/lib.rs b/actix-utils/src/lib.rs
index 82d19ba5..b02687cb 100644
--- a/actix-utils/src/lib.rs
+++ b/actix-utils/src/lib.rs
@@ -6,5 +6,4 @@
 #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
 
 pub mod counter;
-pub mod derive;
 pub mod future;