diff --git a/src/logging.rs b/src/logging.rs
new file mode 100644
index 0000000000000000000000000000000000000000..72a85127fc105475aa10e32082aa8bce6e248104
--- /dev/null
+++ b/src/logging.rs
@@ -0,0 +1,70 @@
+use core::fmt::Display;
+use serenity::{
+    builder::EditMember,
+    http::{client::Http, CacheHttp},
+    model::{
+        channel::{Message, Reaction, ReactionType},
+        guild::Member,
+        id::{ChannelId, RoleId},
+    },
+};
+
+pub trait LoggingReaction {
+    fn try_delete(&self, cache_http: impl CacheHttp);
+}
+
+impl LoggingReaction for Reaction {
+    fn try_delete(&self, cache_http: impl CacheHttp) {
+        self.delete(cache_http)
+            .unwrap_or_else(|e| error!("Unable to delete react: {:?}", e));
+    }
+}
+
+pub trait LoggingMessage {
+    fn try_react<R: Into<ReactionType>>(&self, cache_http: impl CacheHttp, reaction_type: R);
+    fn try_delete(&self, cache_http: impl CacheHttp);
+}
+
+impl LoggingMessage for Message {
+    fn try_react<R: Into<ReactionType>>(&self, cache_http: impl CacheHttp, reaction_type: R) {
+        self.react(cache_http, reaction_type)
+            .unwrap_or_else(|e| error!("Unable to add reaction, {:?}", e));
+    }
+    fn try_delete(&self, cache_http: impl CacheHttp) {
+        self.delete(cache_http)
+            .unwrap_or_else(|e| error!("Unable to delete message: {:?}", e));
+    }
+}
+
+pub trait LoggingChannelId {
+    fn try_say(self, http: impl AsRef<Http>, content: impl Display);
+}
+
+impl LoggingChannelId for ChannelId {
+    fn try_say(self, http: impl AsRef<Http>, content: impl Display) {
+        if let Err(e) = self.say(http, content) {
+            error!("Error sending message {:?}", e)
+        }
+    }
+}
+
+pub trait LoggingMember {
+    fn try_edit<F: FnOnce(&mut EditMember) -> &mut EditMember>(&self, http: impl AsRef<Http>, f: F);
+    fn try_remove_role<R: Into<RoleId>>(&mut self, http: impl AsRef<Http>, role_id: R);
+}
+
+impl LoggingMember for Member {
+    fn try_edit<F: FnOnce(&mut EditMember) -> &mut EditMember>(
+        &self,
+        http: impl AsRef<Http>,
+        f: F,
+    ) {
+        self.edit(http, f)
+            .unwrap_or_else(|e| error!("Unable to edit member: {:?}", e));
+    }
+    fn try_remove_role<R: Into<RoleId>>(&mut self, http: impl AsRef<Http>, role_id: R) {
+        self.remove_role(http, role_id)
+            .unwrap_or_else(|e| error!("Unable to remove role: {:?}", e));
+    }
+}
+
diff --git a/src/main.rs b/src/main.rs
index 2207e5ffd6f9b8e360898d2db279bae27000f3d4..67da86fc226b2b73668cc8c05fd04c595715536b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,6 +18,7 @@ use serenity::{
 };
 
 mod config;
+mod logging;
 mod reaction_roles;
 mod token_management;
 mod user_management;
@@ -25,18 +26,10 @@ mod util;
 mod voting;
 
 use config::CONFIG;
+use logging::*;
 use reaction_roles::{add_role_by_reaction, remove_role_by_reaction};
 use util::get_string_from_react;
 
-macro_rules! e {
-    ($error: literal, $x:expr) => {
-        match $x {
-            Ok(_) => (),
-            Err(why) => error!($error, why),
-        }
-    };
-}
-
 struct Handler;
 
 impl EventHandler for Handler {
@@ -69,12 +62,9 @@ impl EventHandler for Handler {
                 voting::Commands::cowsay(ctx, msg.clone(), message_content[1]);
             }
             "logreact" => {
-                e!("Error deleting logreact prompt: {:?}", msg.delete(&ctx));
-                e!(
-                    "Error sending message {:?}",
-                    msg.channel_id
-                        .say(&ctx.http, "React to this to log the ID (for the next 5min)")
-                )
+                msg.try_delete(&ctx);
+                msg.channel_id
+                    .try_say(&ctx.http, "React to this to log the ID (for the next 5min)")
             }
             "help" => {
                 let mut message = MessageBuilder::new();
@@ -86,20 +76,12 @@ impl EventHandler for Handler {
                     "Use {}poll <proposal> to see what people think about something",
                     &CONFIG.command_prefix
                 ));
-                e!(
-                    "Error sending message: {:?}",
-                    msg.channel_id.say(&ctx.http, message.build())
-                );
-            }
-            _ => {
-                e!(
-                    "Error sending message: {:?}",
-                    msg.channel_id.say(
-                        &ctx.http,
-                        format!("Unrecognised command. Try {}help", &CONFIG.command_prefix)
-                    )
-                );
+                msg.channel_id.try_say(&ctx.http, message.build())
             }
+            _ => msg.channel_id.try_say(
+                &ctx.http,
+                format!("Unrecognised command. Try {}help", &CONFIG.command_prefix),
+            ),
         }
     }
 
@@ -141,10 +123,7 @@ impl EventHandler for Handler {
                             add_reaction.emoji,
                         ));
                         msg.push_mono(react_as_string);
-                        e!(
-                            "Error sending message: {:?}",
-                            message.channel_id.say(&ctx.http, msg.build())
-                        );
+                        message.channel_id.try_say(&ctx.http, msg.build())
                     }
                     _ => {}
                 }
diff --git a/src/reaction_roles.rs b/src/reaction_roles.rs
index 3c4ba9e91c46c7aee1102c645b269f6482239a38..d498ee8bf671414d6d4e7c10ca37c77032672f65 100644
--- a/src/reaction_roles.rs
+++ b/src/reaction_roles.rs
@@ -1,4 +1,5 @@
 use crate::config::{ReactRoleMap, CONFIG};
+use crate::logging::*;
 use crate::util::{get_react_from_string, get_string_from_react};
 use rayon::prelude::*;
 use serenity::{
@@ -8,15 +9,6 @@ use serenity::{
 use std::collections::{HashMap, HashSet};
 use std::iter::FromIterator;
 
-macro_rules! e {
-    ($error: literal, $x:expr) => {
-        match $x {
-            Ok(_) => (),
-            Err(why) => error!($error, why),
-        }
-    };
-}
-
 pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reaction) {
     let user = added_reaction
         .user_id
@@ -48,7 +40,7 @@ pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reactio
             .ok();
     } else {
         warn!("{} provided invalid react for role", user.name);
-        e!("Unable to delete react: {:?}", added_reaction.delete(ctx));
+        added_reaction.try_delete(ctx);
     }
 }
 
@@ -131,10 +123,7 @@ pub fn sync_all_role_reactions(ctx: &Context) {
 
             // ensure bot has reacted
             if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) {
-                e!(
-                    "Unable to add reaction, {:?}",
-                    message.react(ctx, reaction_type)
-                );
+                message.try_react(ctx, reaction_type);
             }
 
             for member in all_members.clone() {
diff --git a/src/user_management.rs b/src/user_management.rs
index 68f6aa94e4fe826ec08982372be5e2932aeb18a0..826fc287411c7d6aa1ee43e231568a4e2ce18ce9 100644
--- a/src/user_management.rs
+++ b/src/user_management.rs
@@ -6,6 +6,7 @@ use serenity::{
 };
 
 use crate::config::CONFIG;
+use crate::logging::*;
 use crate::token_management::*;
 
 macro_rules! e {
@@ -24,16 +25,11 @@ pub fn new_member(ctx: &Context, mut new_member: Member) {
     message.push_line("! Would you care to introduce yourself?");
     message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…");
     message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content");
-    if let Err(why) = CONFIG.welcome_channel.say(&ctx, message.build()) {
-        error!("Error sending message: {:?}", why);
-    }
-
+    CONFIG.welcome_channel.try_say(&ctx, message.build());
     let mut message = MessageBuilder::new();
     message.push(format!("Say hi to {} in ", new_member.display_name()));
     message.mention(&CONFIG.welcome_channel);
-    if let Err(why) = CONFIG.main_channel.say(&ctx, message.build()) {
-        error!("Error sending message: {:?}", why);
-    }
+    CONFIG.main_channel.try_say(&ctx, message.build());
 
     if let Err(why) = new_member.add_role(&ctx.http, CONFIG.unregistered_member_role) {
         error!("Error adding user role: {:?}", why);
@@ -44,20 +40,20 @@ pub struct Commands;
 impl Commands {
     pub fn register(ctx: Context, msg: Message, account_name: &str) {
         if account_name.is_empty() {
-            e!(
-                "Error sending message: {:?}",
-                msg.channel_id
-                    .say(&ctx.http, "Usage: !register <ucc username>")
-            );
+            msg.channel_id
+                .try_say(&ctx.http, "Usage: !register <ucc username>");
             return;
         }
-        e!(
-            "Error sending message: {:?}",
-            // TODO convert to email
-            msg.channel_id
-               .say(&ctx.http, format!("Hey {} here's that token you ordered: {}\nIf this wasn't you just ignore this.", account_name, generate_token(&msg.author, account_name)))
+        // TODO convert to email
+        msg.channel_id.try_say(
+            &ctx.http,
+            format!(
+                "Hey {} here's that token you ordered: {}\nIf this wasn't you just ignore this.",
+                account_name,
+                generate_token(&msg.author, account_name)
+            ),
         );
-        e!("Error deleting register message: {:?}", msg.delete(ctx));
+        msg.try_delete(ctx);
     }
     pub fn verify(ctx: Context, msg: Message, token: &str) {
         match parse_token(&msg.author, token) {
@@ -71,42 +67,34 @@ impl Commands {
                                 "Unable to remove role: {:?}",
                                 member.remove_role(&ctx.http, CONFIG.unregistered_member_role)
                             );
-                            e!(
-                                "Unable to edit nickname: {:?}",
-                                member.edit(&ctx.http, |m| {
-                                    let mut rng = rand::thread_rng();
-                                    m.nickname(format!(
-                                        "{}, {}",
-                                        name,
-                                        [
-                                            "The Big Cheese",
-                                            "The One and Only",
-                                            "The Exalted One",
-                                            "not to be trusted",
-                                            "The Scoundrel",
-                                            "A big fish in a small pond",
-                                        ][rng.gen_range(0, 5)]
-                                    ));
-                                    m
-                                })
-                            );
+                            member.try_edit(&ctx.http, |m| {
+                                let mut rng = rand::thread_rng();
+                                m.nickname(format!(
+                                    "{}, {}",
+                                    name,
+                                    [
+                                        "The Big Cheese",
+                                        "The One and Only",
+                                        "The Exalted One",
+                                        "not to be trusted",
+                                        "The Scoundrel",
+                                        "A big fish in a small pond",
+                                    ][rng.gen_range(0, 5)]
+                                ));
+                                m
+                            });
                             let new_msg = msg
                                 .channel_id
                                 .say(&ctx.http, "Verification succesful")
                                 .expect("Error sending message");
-                            e!(
-                                "Error deleting register message: {:?}",
-                                new_msg.delete(&ctx)
-                            );
+                            new_msg.try_delete(&ctx);
                         })
                 );
             }
-            Err(reason) => e!(
-                "Error sending message: {:?}",
-                msg.channel_id
-                    .say(&ctx.http, format!("Verification error: {:?}", reason))
-            ),
+            Err(reason) => msg
+                .channel_id
+                .try_say(&ctx.http, format!("Verification error: {:?}", reason)),
         }
-        e!("Error deleting register message: {:?}", msg.delete(&ctx));
+        msg.try_delete(&ctx);
     }
 }
diff --git a/src/voting.rs b/src/voting.rs
index ebb3e592a3662e0e5e556b8e69e6c1ca3e467b06..a2faf1e581a09f5663f62340fa9aa5177269fe66 100644
--- a/src/voting.rs
+++ b/src/voting.rs
@@ -7,18 +7,11 @@ use std::collections::HashMap;
 use std::sync::Mutex;
 
 use crate::config::CONFIG;
+use crate::logging::*;
 use crate::util::get_string_from_react;
 
-macro_rules! e {
-    ($error: literal, $x:expr) => {
-        match $x {
-            Ok(_) => (),
-            Err(why) => error!($error, why),
-        }
-    };
-}
-
 pub struct Commands;
+
 impl Commands {
     pub fn move_something(ctx: Context, msg: Message, content: &str) {
         let motion = content;
@@ -26,20 +19,16 @@ impl Commands {
             create_motion(&ctx, &msg, motion);
             return;
         }
-        e!(
-            "Error sending message: {:?}",
-            msg.channel_id.say(
-                &ctx.http,
-                "If there's something you want to motion, put it after the !move keyword",
-            )
+        msg.channel_id.try_say(
+            &ctx.http,
+            "If there's something you want to motion, put it after the !move keyword",
         );
     }
     pub fn motion(ctx: Context, msg: Message, _content: &str) {
-        e!("Error sending message: {:?}",
-                msg.channel_id.say(
-                &ctx.http,
-                "I hope you're not having a motion. You may have wanted to !move something instead."
-            ));
+        msg.channel_id.try_say(
+            &ctx.http,
+            "I hope you're not having a motion. You may have wanted to !move something instead.",
+        );
     }
     pub fn poll(ctx: Context, msg: Message, content: &str) {
         let topic = content;
@@ -47,12 +36,9 @@ impl Commands {
             create_poll(&ctx, &msg, topic);
             return;
         }
-        e!(
-            "Error sending message: {:?}",
-            msg.channel_id.say(
-                &ctx.http,
-                "If there's something you want to motion, put it after the !move keyword",
-            )
+        msg.channel_id.try_say(
+            &ctx.http,
+            "If there's something you want to motion, put it after the !move keyword",
         );
     }
     pub fn cowsay(ctx: Context, msg: Message, content: &str) {
@@ -78,10 +64,7 @@ impl Commands {
             String::from_utf8(output.stdout).expect("unable to parse stdout to String"),
             None,
         );
-        e!(
-            "Error sending message: {:?}",
-            msg.channel_id.say(&ctx.http, message.build())
-        );
+        msg.channel_id.try_say(&ctx.http, message.build());
     }
 }
 
@@ -284,9 +267,9 @@ fn update_motion(
             message.push(" is now ");
             message.push_bold(status);
             message.push_italic(format!(" (was {})", last_status));
-            if let Err(why) = CONFIG.announcement_channel.say(&ctx.http, message.build()) {
-                error!("Error sending message: {:?}", why);
-            };
+            CONFIG
+                .announcement_channel
+                .try_say(&ctx.http, message.build());
         }
     };