Escaping output in WordPress themes: the short version
Any value that comes from the database, a custom field or a user can contain something unexpected. Escaping output in WordPress makes sure it is printed as data, not as code. The rule I follow is simple: escape late, at the moment of output, and for the context you are printing into.
The everyday functions
esc_html()for text inside HTML elementsesc_attr()for values inside HTML attributesesc_url()for links and image sourceswp_kses_post()for content that is allowed to contain post-style HTML
Examples
<h2 class="card__title"><?php echo esc_html( $title ); ?></h2>
<a href="<?php echo esc_url( $link['url'] ); ?>"
class="<?php echo esc_attr( $classes ); ?>">
<?php echo esc_html( $link['title'] ); ?>
</a>
<div class="card__body"><?php echo wp_kses_post( $biography ); ?></div>
Custom fields are not trusted
It is easy to assume a value from an SCF or ACF field is safe because an editor typed it. Treat it like any other input. A pasted script tag in a text field should print as text, not run.
Sanitise on the way in, escape on the way out
Sanitising cleans data before saving, with functions like sanitize_text_field() and sanitize_email(). Escaping protects output. They solve different problems, so you need both.
Translation strings too
Use esc_html_e() and esc_attr__() for translatable strings. Translations come from files you do not control.