Index: src/bp-activity/bp-activity-filters.php
===================================================================
--- src/bp-activity/bp-activity-filters.php
+++ src/bp-activity/bp-activity-filters.php
@@ -152,8 +152,14 @@
 
 	// Unset the activity component so activity stream update fails
 	// @todo This is temporary until some kind of moderation is built.
-	if ( !bp_core_check_for_moderation( $activity->user_id, '', $activity->content ) )
+	$moderate = bp_core_check_for_moderation( $activity->user_id, '', $activity->content, 'wp_error' );
+	if ( is_wp_error( $moderate ) ) {
+		// Send back the same error object.
+		$activity->errors = $moderate;
+
+		// Backpat.
 		$activity->component = false;
+	}
 }
 
 /**
@@ -169,9 +175,17 @@
 	if ( ! in_array( $activity->type, bp_activity_get_moderated_activity_types() ) )
 		return;
 
-	// Mark as spam.
-	if ( ! bp_core_check_for_blacklist( $activity->user_id, '', $activity->content ) )
+	$blacklist = bp_core_check_for_blacklist( $activity->user_id, '', $activity->content, 'wp_error' );
+
+	// If content matches blacklist, do something.
+	if ( is_wp_error( $blacklist ) ) {
+		// Mark activity as spam.
+		// @todo Should we just block blacklist items from being saved entirely?		
 		bp_activity_mark_as_spam( $activity, 'by_blacklist' );
+
+		// Send back the same error object.
+		//$activity->errors = $blacklist;
+	}
 }
 
 /**
Index: src/bp-activity/bp-activity-functions.php
===================================================================
--- src/bp-activity/bp-activity-functions.php
+++ src/bp-activity/bp-activity-functions.php
@@ -1836,12 +1836,7 @@
  * Add an activity item.
  *
  * @since 1.1.0
- *
- * @uses wp_parse_args()
- * @uses BP_Activity_Activity::save() {@link BP_Activity_Activity}
- * @uses BP_Activity_Activity::rebuild_activity_comment_tree() {@link BP_Activity_Activity}
- * @uses wp_cache_delete()
- * @uses do_action() To call the 'bp_activity_add' hook.
+ * @since 2.6.0 Added 'error_type' parameter to $args.
  *
  * @param array|string $args {
  *     An array of arguments.
@@ -1873,6 +1868,7 @@
  *     @type bool     $hide_sitewide     Should the item be hidden on sitewide streams?
  *                                       Default: false.
  *     @type bool     $is_spam           Should the item be marked as spam? Default: false.
+ *     @type string   $error_type        Optional. Error type. Either 'bool' or 'wp_error'. Default: 'bool'.
  * }
  * @return int|bool The ID of the activity on success. False on error.
  */
@@ -1891,6 +1887,7 @@
 		'recorded_time'     => bp_core_current_time(), // The GMT time that this activity was recorded.
 		'hide_sitewide'     => false,                  // Should this be hidden on the sitewide activity stream?
 		'is_spam'           => false,                  // Is this activity item to be marked as spam?
+		'error_type'        => 'bool'
 	), 'activity_add' );
 
 	// Make sure we are backwards compatible.
@@ -1914,11 +1911,16 @@
 	$activity->date_recorded     = $r['recorded_time'];
 	$activity->hide_sitewide     = $r['hide_sitewide'];
 	$activity->is_spam           = $r['is_spam'];
+	$activity->error_type        = $r['error_type'];
 	$activity->action            = ! empty( $r['action'] )
-										? $r['action']
-										: bp_activity_generate_action_string( $activity );
+						? $r['action']
+						: bp_activity_generate_action_string( $activity );
 
-	if ( ! $activity->save() ) {
+	$save = $activity->save();
+
+	if ( 'wp_error' === $r['error_type'] && is_wp_error( $save ) ) {
+		return $save;
+	} elseif ('bool' === $r['error_type'] && false === $save ) {
 		return false;
 	}
 
@@ -1970,7 +1972,8 @@
 
 	$r = wp_parse_args( $args, array(
 		'content' => false,
-		'user_id' => bp_loggedin_user_id()
+		'user_id' => bp_loggedin_user_id(),
+		'error_type' => 'bool',
 	) );
 
 	if ( empty( $r['content'] ) || !strlen( trim( $r['content'] ) ) ) {
@@ -2010,8 +2013,13 @@
 		'primary_link' => $add_primary_link,
 		'component'    => buddypress()->activity->id,
 		'type'         => 'activity_update',
+		'error_type'   => $r['error_type']
 	) );
 
+	if ( is_wp_error( $activity_id ) ) {
+		return $activity_id;
+	}
+
 	/**
 	 * Filters the latest update content for the activity item.
 	 *
@@ -2564,6 +2572,7 @@
  * @since 1.2.0
  * @since 2.5.0 Add a new possible parameter $skip_notification for the array of arguments.
  *              Add the $primary_link parameter for the array of arguments.
+ * @since 2.6.0 Added 'error_type' parameter to $args.
  *
  * @uses wp_parse_args()
  * @uses bp_activity_add()
@@ -2586,17 +2595,12 @@
  *                                     Defaults to an empty string.
  *     @type bool   $skip_notification Optional. false to send a comment notification, false otherwise.
  *                                     Defaults to false.
+ *     @type string $error_type        Optional. Error type. Either 'bool' or 'wp_error'. Default: 'bool'.
  * }
  * @return int|bool The ID of the comment on success, otherwise false.
  */
 function bp_activity_new_comment( $args = '' ) {
-	$bp       = buddypress();
-	$errors   = new WP_Error();
-	$feedback = __( 'There was an error posting your reply. Please try again.', 'buddypress' );
-
-	if ( empty( $bp->activity->errors ) ) {
-		$bp->activity->errors = array();
-	}
+	$bp = buddypress();
 
 	$r = wp_parse_args( $args, array(
 		'id'                => false,
@@ -2606,14 +2610,31 @@
 		'parent_id'         => false, // ID of a parent comment (optional).
 		'primary_link'      => '',
 		'skip_notification' => false,
+		'error_type'        => 'bool'
 	) );
 
+	// Error type is boolean; need to initialize some variables for backpat.
+	if ( 'bool' === $r['error_type'] ) {
+		if ( empty( $bp->activity->errors ) ) {
+			$bp->activity->errors = array();
+		}
+	}
+
+	// Default error message.
+	$feedback = __( 'There was an error posting your reply. Please try again.', 'buddypress' );
+
 	// Bail if missing necessary data.
 	if ( empty( $r['content'] ) || empty( $r['user_id'] ) || empty( $r['activity_id'] ) ) {
-		$errors->add( 'missing_data', $feedback );
-		$bp->activity->errors['new_comment'] = $errors;
+		$error = new WP_Error( 'missing_data', $feedback );
 
-		return false;
+		if ( 'wp_error' === $r['error_type'] ) {
+			return $error;
+
+		// Backpat.
+		} else {
+			$bp->activity->errors['new_comment'] = $error;
+			return false;
+		}
 	}
 
 	// Maybe set current activity ID as the parent.
@@ -2628,10 +2649,17 @@
 
 	// Bail if the parent activity does not exist.
 	if ( empty( $activity->date_recorded ) ) {
-		$errors->add( 'missing_activity', __( 'Sorry, the item you are replying to no longer exists.', 'buddypress' ) );
-		$bp->activity->errors['new_comment'] = $errors;
+		$error = new WP_Error( 'missing_activity', __( 'The item you were replying to no longer exists.', 'buddypress' ) );
+
+		if ( 'wp_error' === $r['error_type'] ) {
+			return $error;
+
+		// Backpat.
+		} else {
+			$bp->activity->errors['new_comment'] = $error;
+			return false;
+		}
 
-		return false;
 	}
 
 	// Check to see if the parent activity is hidden, and if so, hide this comment publicly.
@@ -2656,9 +2684,15 @@
 		'user_id'           => $r['user_id'],
 		'item_id'           => $activity_id,
 		'secondary_item_id' => $r['parent_id'],
-		'hide_sitewide'     => $is_hidden
+		'hide_sitewide'     => $is_hidden,
+		'error_type'        => $r['error_type']
 	) );
 
+	// Return WP Error.
+	if ( is_wp_error( $comment_id ) && 'wp_error' === $r['error_type'] ) {
+		return $comment_id;
+	}
+
 	// Comment caches are stored only with the top-level item.
 	wp_cache_delete( $activity_id, 'bp_activity_comments' );
 
@@ -2698,8 +2732,15 @@
 	}
 
 	if ( empty( $comment_id ) ) {
-		$errors->add( 'comment_failed', $feedback );
-		$bp->activity->errors['new_comment'] = $errors;
+		$error = new WP_Error( 'comment_failed', $feedback );
+
+		if ( 'wp_error' === $r['error_type'] ) {
+			return $error;
+
+		// Backpat.
+		} else {
+			$bp->activity->errors['new_comment'] = $error;
+		}
 	}
 
 	return $comment_id;
@@ -3387,6 +3428,8 @@
  *
  * @since 1.6.0
  *
+ * @todo We should probably save $source to activity meta.
+ *
  * @param BP_Activity_Activity $activity The activity item to be spammed.
  * @param string               $source   Optional. Default is "by_a_person" (ie, a person has
  *                                       manually marked the activity as spam). BP core also
Index: src/bp-activity/classes/class-bp-activity-activity.php
===================================================================
--- src/bp-activity/classes/class-bp-activity-activity.php
+++ src/bp-activity/classes/class-bp-activity-activity.php
@@ -134,6 +134,24 @@
 	var $is_spam;
 
 	/**
+	 * Error holder.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var WP_Error
+	 */
+	public $errors;
+
+	/**
+	 * Error type to return. Either 'bool' or 'wp_error'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var string
+	 */
+	public $error_type = 'bool';
+
+	/**
 	 * Constructor method.
 	 *
 	 * @since 1.5.0
@@ -141,6 +159,9 @@
 	 * @param int|bool $id Optional. The ID of a specific activity item.
 	 */
 	public function __construct( $id = false ) {
+		// Instantiate errors object.
+		$this->errors = new WP_Error;
+
 		if ( !empty( $id ) ) {
 			$this->id = $id;
 			$this->populate();
@@ -235,8 +256,22 @@
 		 */
 		do_action_ref_array( 'bp_activity_before_save', array( &$this ) );
 
+		if ( 'wp_error' === $this->error_type && ! empty( $this->errors->errors ) ) {
+			return $this->errors;
+		}
+
 		if ( empty( $this->component ) || empty( $this->type ) ) {
-			return false;
+			if ( 'bool' === $this->error_type ) {
+				return false;
+			} else {
+				if ( empty( $this->component ) ) {
+					$this->errors->add( 'bp_activity_missing_component' );
+				} else {
+					$this->errors->add( 'bp_activity_missing_type' );
+				}
+
+				return $this->errors;
+			}
 		}
 
 		if ( empty( $this->primary_link ) ) {
Index: src/bp-core/bp-core-moderation.php
===================================================================
--- src/bp-core/bp-core-moderation.php
+++ src/bp-core/bp-core-moderation.php
@@ -52,17 +52,15 @@
  * Check for moderation keys and too many links.
  *
  * @since 1.6.0
+ * @since 2.x.0 Added $error_type parameter.
  *
- * @uses bp_current_author_ip() To get current user IP address.
- * @uses bp_current_author_ua() To get current user agent.
- * @uses bp_current_user_can() Allow super admins to bypass blacklist.
- *
- * @param int    $user_id Topic or reply author ID.
- * @param string $title   The title of the content.
- * @param string $content The content being posted.
+ * @param int    $user_id    Topic or reply author ID.
+ * @param string $title      The title of the content.
+ * @param string $content    The content being posted.
+ * @param string $error_type The error type to return. Either 'bool' or 'wp_error'.
  * @return bool True if test is passed, false if fail.
  */
-function bp_core_check_for_moderation( $user_id = 0, $title = '', $content = '' ) {
+function bp_core_check_for_moderation( $user_id = 0, $title = '', $content = '', $error_type = 'bool' ) {
 
 	/**
 	 * Filters whether or not to bypass checking for moderation keys and too many links.
@@ -136,7 +134,11 @@
 
 		// Das ist zu viele links!
 		if ( $num_links >= $max_links ) {
-			return false;
+			if ( 'bool' === $error_type ) {
+				return false;
+			} else {
+				return new WP_Error( 'bp_moderation_too_many_links', __( 'You have inputted too many links', 'buddypress' ) );
+			}
 		}
 	}
 
@@ -173,9 +175,11 @@
 
 				// Check each user data for current word.
 				if ( preg_match( $pattern, $post_data ) ) {
-
-					// Post does not pass.
-					return false;
+					if ( 'bool' === $error_type ) {
+						return false;
+					} else {
+						return new WP_Error( 'bp_moderation_blacklist_match', __( 'You have inputted an inappropriate word.', 'buddypress' ) );
+					}
 				}
 			}
 		}
@@ -189,17 +193,17 @@
  * Check for blocked keys.
  *
  * @since 1.6.0
+ * @since 2.x.0 Added $error_type parameter.
  *
- * @uses bp_current_author_ip() To get current user IP address.
- * @uses bp_current_author_ua() To get current user agent.
- * @uses bp_current_user_can() Allow super admins to bypass blacklist.
+ * @todo Why don't we use wp_blacklist_check() for this?
  *
- * @param int    $user_id Topic or reply author ID.
- * @param string $title   The title of the content.
- * @param string $content The content being posted.
+ * @param int    $user_id    Topic or reply author ID.
+ * @param string $title      The title of the content.
+ * @param string $content    The content being posted.
+ * @param string $error_type The error type to return. Either 'bool' or 'wp_error'.
  * @return bool True if test is passed, false if fail.
  */
-function bp_core_check_for_blacklist( $user_id = 0, $title = '', $content = '' ) {
+function bp_core_check_for_blacklist( $user_id = 0, $title = '', $content = '', $error_type = 'bool' ) {
 
 	/**
 	 * Filters whether or not to bypass checking for blocked keys.
@@ -284,9 +288,11 @@
 
 			// Check each user data for current word.
 			if ( preg_match( $pattern, $post_data ) ) {
-
-				// Post does not pass.
-				return false;
+				if ( 'bool' === $error_type ) {
+					return false;
+				} else {
+					return new WP_Error( 'bp_moderation_blacklist_match', __( 'You have inputted an inappropriate word.', 'buddypress' ) );
+				}
 			}
 		}
 	}
Index: src/bp-groups/bp-groups-activity.php
===================================================================
--- src/bp-groups/bp-groups-activity.php
+++ src/bp-groups/bp-groups-activity.php
@@ -379,7 +379,8 @@
 		'item_id'           => false,
 		'secondary_item_id' => false,
 		'recorded_time'     => bp_core_current_time(),
-		'hide_sitewide'     => $hide_sitewide
+		'hide_sitewide'     => $hide_sitewide,
+		'error_type'        => 'bool'
 	) );
 
 	return bp_activity_add( $r );
Index: src/bp-groups/bp-groups-functions.php
===================================================================
--- src/bp-groups/bp-groups-functions.php
+++ src/bp-groups/bp-groups-functions.php
@@ -944,6 +944,7 @@
  * Post an Activity status update affiliated with a group.
  *
  * @since 1.2.0
+ * @since 2.x.0 Added 'error_type' parameter to $args.
  *
  * @param array|string $args {
  *     Array of arguments.
@@ -963,9 +964,10 @@
 	$bp = buddypress();
 
 	$defaults = array(
-		'content'  => false,
-		'user_id'  => bp_loggedin_user_id(),
-		'group_id' => 0
+		'content'    => false,
+		'user_id'    => bp_loggedin_user_id(),
+		'group_id'   => 0,
+		'error_type' => 'bool'
 	);
 
 	$r = wp_parse_args( $args, $defaults );
@@ -1006,11 +1008,12 @@
 	$content_filtered = apply_filters( 'groups_activity_new_update_content', $activity_content );
 
 	$activity_id = groups_record_activity( array(
-		'user_id' => $user_id,
-		'action'  => $action,
-		'content' => $content_filtered,
-		'type'    => 'activity_update',
-		'item_id' => $group_id
+		'user_id'    => $user_id,
+		'action'     => $action,
+		'content'    => $content_filtered,
+		'type'       => 'activity_update',
+		'item_id'    => $group_id,
+		'error_type' => $error_type
 	) );
 
 	groups_update_groupmeta( $group_id, 'last_activity', bp_core_current_time() );
Index: src/bp-templates/bp-legacy/buddypress-functions.php
===================================================================
--- src/bp-templates/bp-legacy/buddypress-functions.php
+++ src/bp-templates/bp-legacy/buddypress-functions.php
@@ -927,11 +927,11 @@
 	}
 
 	if ( ! $object && bp_is_active( 'activity' ) ) {
-		$activity_id = bp_activity_post_update( array( 'content' => $_POST['content'] ) );
+		$activity_id = bp_activity_post_update( array( 'content' => $_POST['content'], 'error_type' => 'wp_error' ) );
 
 	} elseif ( 'groups' === $object ) {
 		if ( $item_id && bp_is_active( 'groups' ) )
-			$activity_id = groups_post_update( array( 'content' => $_POST['content'], 'group_id' => $item_id ) );
+			$activity_id = groups_post_update( array( 'content' => $_POST['content'], 'group_id' => $item_id, 'error_type' => 'wp_error' ) );
 
 	} else {
 
@@ -939,8 +939,11 @@
 		$activity_id = apply_filters( 'bp_activity_custom_update', false, $object, $item_id, $_POST['content'] );
 	}
 
-	if ( empty( $activity_id ) )
+	if ( false === $activity_id ) {
 		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . __( 'There was a problem posting your update. Please try again.', 'buddypress' ) . '</p></div>' );
+	} elseif ( is_wp_error( $activity_id ) && ! empty( $activity_id->errors ) ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . $activity_id->get_error_message() . '</p></div>' );
+	}
 
 	$last_recorded = ! empty( $_POST['since'] ) ? date( 'Y-m-d H:i:s', intval( $_POST['since'] ) ) : 0;
 	if ( $last_recorded ) {
@@ -1005,15 +1008,11 @@
 		'activity_id' => $_POST['form_id'],
 		'content'     => $_POST['content'],
 		'parent_id'   => $_POST['comment_id'],
+		'error_type'  => 'wp_error'
 	) );
 
-	if ( ! $comment_id ) {
-		if ( ! empty( $bp->activity->errors['new_comment'] ) && is_wp_error( $bp->activity->errors['new_comment'] ) ) {
-			$feedback = $bp->activity->errors['new_comment']->get_error_message();
-			unset( $bp->activity->errors['new_comment'] );
-		}
-
-		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . esc_html( $feedback ) . '</p></div>' );
+	if ( is_wp_error( $comment_id ) ) {
+		exit( '-1<div id="message" class="error bp-ajax-message"><p>' . esc_html( $comment_id->get_error_message() ) . '</p></div>' );
 	}
 
 	// Load the new activity item into the $activities_template global.
