These three terms get used interchangeably in a lot of WordPress tutorials, which is part of why plugins keep shipping SQL injection and XSS bugs. They’re not the same thing, and they happen at different points in a request’s lifecycle.
Sanitization: cleaning input on the way in
This happens once, when data arrives, before it’s stored anywhere:
$email = sanitize_email($_POST['email']);
$title = sanitize_text_field($_POST['title']);
$html = wp_kses_post($_POST['content']); // allows safe HTML tags only
Sanitize based on what the data is, not where it’s going next — an email field gets sanitize_email() regardless of whether it’ll later be displayed or stored.
Escaping: protecting output on the way out
This happens every time data is printed, at the point it’s printed, not before:
echo '<h1>' . esc_html($title) . '</h1>';
echo '<a href="' . esc_url($link) . '">Click</a>';
echo '<input value="' . esc_attr($value) . '">';
The common mistake is escaping once at storage time and assuming it’s covered — but the same piece of data might get echoed into an HTML attribute in one place and inside a URL in another, and each context needs its own escaping function.
Prepared statements: SQL specifically
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}orders WHERE customer_id = %d AND status = %s",
$customer_id,
$status
)
);
%d, %s, and %f tell $wpdb exactly how to escape each value for its type — string concatenation into a raw query string is where SQL injection actually happens, even with sanitized input, because sanitization alone doesn’t account for SQL syntax.
The rule that avoids most bugs here: sanitize on input, escape on output, prepare every query — and never assume one of the three covers for the other two.