diff --git a/packages/backend-rs/src/database/cache.rs b/packages/backend-rs/src/database/cache.rs
index 515053ef6c..d56da7f680 100644
--- a/packages/backend-rs/src/database/cache.rs
+++ b/packages/backend-rs/src/database/cache.rs
@@ -14,7 +14,7 @@ pub enum Category {
     Test,
 }
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("failed to execute Redis command")]
     Redis(#[from] RedisError),
diff --git a/packages/backend-rs/src/database/redis.rs b/packages/backend-rs/src/database/redis.rs
index 7fa7255b47..a17271f44f 100644
--- a/packages/backend-rs/src/database/redis.rs
+++ b/packages/backend-rs/src/database/redis.rs
@@ -82,7 +82,7 @@ async fn init_conn_pool() -> Result<(), RedisError> {
     Ok(())
 }
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum RedisConnError {
     #[error("failed to initialize Redis connection pool")]
     Redis(RedisError),
diff --git a/packages/backend-rs/src/federation/internal_actor/cache.rs b/packages/backend-rs/src/federation/internal_actor/cache.rs
index 80f0ad22bc..de250f3d20 100644
--- a/packages/backend-rs/src/federation/internal_actor/cache.rs
+++ b/packages/backend-rs/src/federation/internal_actor/cache.rs
@@ -7,7 +7,7 @@ use crate::{database::db_conn, model::entity::user};
 use sea_orm::prelude::*;
 use std::sync::Mutex;
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error(transparent)]
     #[doc = "database error"]
diff --git a/packages/backend-rs/src/federation/nodeinfo/fetch.rs b/packages/backend-rs/src/federation/nodeinfo/fetch.rs
index c237a893fb..e7bdb0b071 100644
--- a/packages/backend-rs/src/federation/nodeinfo/fetch.rs
+++ b/packages/backend-rs/src/federation/nodeinfo/fetch.rs
@@ -7,7 +7,7 @@ use isahc::AsyncReadResponseExt;
 use serde::Deserialize;
 
 /// Errors that can occur while fetching NodeInfo from a remote server
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("failed to acquire an HTTP client")]
     HttpClient(#[from] http_client::Error),
diff --git a/packages/backend-rs/src/federation/nodeinfo/generate.rs b/packages/backend-rs/src/federation/nodeinfo/generate.rs
index 6cad9e59ae..4ab7ef65fa 100644
--- a/packages/backend-rs/src/federation/nodeinfo/generate.rs
+++ b/packages/backend-rs/src/federation/nodeinfo/generate.rs
@@ -153,7 +153,7 @@ pub async fn nodeinfo_2_0() -> Result<Nodeinfo20, DbErr> {
 }
 
 #[cfg(any(test, doctest, feature = "napi"))]
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[doc = "database error"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/misc/convert_host.rs b/packages/backend-rs/src/misc/convert_host.rs
index 0de4d1912d..fff1f9fb4c 100644
--- a/packages/backend-rs/src/misc/convert_host.rs
+++ b/packages/backend-rs/src/misc/convert_host.rs
@@ -2,7 +2,7 @@
 // We may want to (re)implement these functions in the `federation` module
 // in a Rusty way (e.g., traits of actor type) if needed.
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[doc = "UTS #46 process has failed"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/misc/get_image_size.rs b/packages/backend-rs/src/misc/get_image_size.rs
index bc5222f054..a6c06df5f9 100644
--- a/packages/backend-rs/src/misc/get_image_size.rs
+++ b/packages/backend-rs/src/misc/get_image_size.rs
@@ -5,7 +5,7 @@ use nom_exif::{parse_jpeg_exif, EntryValue, ExifTag};
 use std::io::Cursor;
 use tokio::sync::Mutex;
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("Redis cache operation has failed")]
     Cache(#[from] cache::Error),
diff --git a/packages/backend-rs/src/misc/latest_version.rs b/packages/backend-rs/src/misc/latest_version.rs
index 1f330f4d2e..f49ac81099 100644
--- a/packages/backend-rs/src/misc/latest_version.rs
+++ b/packages/backend-rs/src/misc/latest_version.rs
@@ -4,7 +4,7 @@ use crate::{database::cache, util::http_client};
 use isahc::AsyncReadResponseExt;
 use serde::Deserialize;
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("Redis cache operation has failed")]
     Cache(#[from] cache::Error),
diff --git a/packages/backend-rs/src/misc/password.rs b/packages/backend-rs/src/misc/password.rs
index bc2025f275..8d6be4101e 100644
--- a/packages/backend-rs/src/misc/password.rs
+++ b/packages/backend-rs/src/misc/password.rs
@@ -15,7 +15,7 @@ pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Er
         .to_string())
 }
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("failed to verify password against bcrypt hash")]
     Bcrypt(#[from] bcrypt::BcryptError),
diff --git a/packages/backend-rs/src/misc/reaction.rs b/packages/backend-rs/src/misc/reaction.rs
index e6b699d59a..afae0a66ac 100644
--- a/packages/backend-rs/src/misc/reaction.rs
+++ b/packages/backend-rs/src/misc/reaction.rs
@@ -53,7 +53,7 @@ pub fn count_reactions(reactions: &HashMap<String, u32>) -> HashMap<String, u32>
     res
 }
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[doc = "UTS #46 process has failed"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/misc/should_nyaify.rs b/packages/backend-rs/src/misc/should_nyaify.rs
index bf4166f18d..8476678c77 100644
--- a/packages/backend-rs/src/misc/should_nyaify.rs
+++ b/packages/backend-rs/src/misc/should_nyaify.rs
@@ -6,7 +6,7 @@ use crate::{
 };
 use sea_orm::{DbErr, EntityTrait, QuerySelect, SelectColumns};
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[doc = "database error"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/service/antenna/check_hit.rs b/packages/backend-rs/src/service/antenna/check_hit.rs
index 794b2f794e..0ef8f13595 100644
--- a/packages/backend-rs/src/service/antenna/check_hit.rs
+++ b/packages/backend-rs/src/service/antenna/check_hit.rs
@@ -6,7 +6,7 @@ use crate::{
 };
 use sea_orm::{prelude::*, QuerySelect};
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum AntennaCheckError {
     #[doc = "database error"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/service/antenna/process_new_note.rs b/packages/backend-rs/src/service/antenna/process_new_note.rs
index 0588c3c3f3..d9099f867f 100644
--- a/packages/backend-rs/src/service/antenna/process_new_note.rs
+++ b/packages/backend-rs/src/service/antenna/process_new_note.rs
@@ -13,7 +13,7 @@ use crate::{
 use redis::{streams::StreamMaxlen, AsyncCommands, RedisError};
 use sea_orm::prelude::*;
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[doc = "database error"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/service/push_notification.rs b/packages/backend-rs/src/service/push_notification.rs
index cfe1d99ae8..cbf3be0793 100644
--- a/packages/backend-rs/src/service/push_notification.rs
+++ b/packages/backend-rs/src/service/push_notification.rs
@@ -13,7 +13,7 @@ use sea_orm::prelude::*;
 use serde::Deserialize;
 use web_push::*;
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[doc = "database error"]
     #[error(transparent)]
diff --git a/packages/backend-rs/src/service/stream.rs b/packages/backend-rs/src/service/stream.rs
index 25f5802eef..d1acbfa4a7 100644
--- a/packages/backend-rs/src/service/stream.rs
+++ b/packages/backend-rs/src/service/stream.rs
@@ -62,7 +62,7 @@ pub enum ChatEvent {
     Typing,
 }
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("failed to execute a Redis command")]
     Redis(#[from] RedisError),
diff --git a/packages/backend-rs/src/util/error_chain.rs b/packages/backend-rs/src/util/error_chain.rs
index d19c99ae76..59af604578 100644
--- a/packages/backend-rs/src/util/error_chain.rs
+++ b/packages/backend-rs/src/util/error_chain.rs
@@ -34,7 +34,7 @@ mod unit_test {
         #[error("unexpected string '{0}'")]
         struct InnerError2(String);
 
-        #[derive(thiserror::Error, Debug)]
+        #[macros::errors]
         enum ErrorVariants {
             #[error("error 1 occured")]
             Error1(#[from] InnerError1),
diff --git a/packages/backend-rs/src/util/http_client.rs b/packages/backend-rs/src/util/http_client.rs
index 3b56d57c6e..0347334b35 100644
--- a/packages/backend-rs/src/util/http_client.rs
+++ b/packages/backend-rs/src/util/http_client.rs
@@ -5,7 +5,7 @@ use isahc::{config::*, HttpClient};
 use once_cell::sync::OnceCell;
 use std::time::Duration;
 
-#[derive(thiserror::Error, Debug)]
+#[macros::errors]
 pub enum Error {
     #[error("HTTP request failed")]
     Isahc(#[from] isahc::Error),
diff --git a/packages/macro-rs/macros-impl/src/error.rs b/packages/macro-rs/macros-impl/src/error.rs
new file mode 100644
index 0000000000..a5d6fe4d38
--- /dev/null
+++ b/packages/macro-rs/macros-impl/src/error.rs
@@ -0,0 +1,41 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+
+pub fn error_variants(_attr: TokenStream, item: TokenStream) -> TokenStream {
+    match error_variants_impl(item) {
+        Ok(tokens) => tokens,
+        Err(error) => error.to_compile_error(),
+    }
+}
+
+fn error_variants_impl(item: TokenStream) -> syn::Result<TokenStream> {
+    let items: syn::ItemEnum = syn::parse2(item.clone())?;
+    let mut new_item = items.clone();
+
+    new_item.variants = items
+        .variants
+        .into_iter()
+        .map(|mut variant| {
+            // check if doc attribute is alredy there
+            if variant.attrs.iter().any(|attr| attr.path().is_ident("doc")) {
+                return variant;
+            }
+
+            let msg = variant.attrs.iter().find_map(|attr| {
+                if !attr.path().is_ident("error") {
+                    return None;
+                }
+                let lit: syn::LitStr = attr.parse_args().ok()?;
+                Some(lit.value())
+            });
+
+            if let Some(msg) = msg {
+                variant.attrs.push(syn::parse_quote! { #[doc = #msg] });
+            }
+
+            variant
+        })
+        .collect();
+
+    Ok(quote! { #new_item })
+}
diff --git a/packages/macro-rs/macros-impl/src/lib.rs b/packages/macro-rs/macros-impl/src/lib.rs
index 9e4a70240e..4d9cde075b 100644
--- a/packages/macro-rs/macros-impl/src/lib.rs
+++ b/packages/macro-rs/macros-impl/src/lib.rs
@@ -1,4 +1,5 @@
 #![allow(clippy::items_after_test_module)]
 
+pub mod error;
 pub mod napi;
 mod util;
diff --git a/packages/macro-rs/macros/src/lib.rs b/packages/macro-rs/macros/src/lib.rs
index cadb00475d..3619839a74 100644
--- a/packages/macro-rs/macros/src/lib.rs
+++ b/packages/macro-rs/macros/src/lib.rs
@@ -75,10 +75,20 @@ define_wrapper_proc_macro_attributes! {
         #[cfg(any(test, doctest))]
         #item
     }
+
+    /// When applied to error variant enums, this macro generates a document
+    /// based on error messages unless there is a doc comment
+    errors(attr, item) {
+        #[derive(::thiserror::Error, ::std::fmt::Debug)]
+        #[macros::error_variants(#attr, #item)]
+        #item
+    }
 }
 
 reexport_proc_macro_attributes! {
     /// Creates an extra wrapper function for [napi_derive](https://docs.rs/napi-derive/latest/napi_derive/).
     /// See [macros_impl::napi::napi] for details.
     macros_impl::napi::napi as napi
+
+    macros_impl::error::error_variants as error_variants
 }