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.