Cross-site scripting is the vulnerability where an attacker gets their JavaScript running in another user's browser, on your domain, with all the access that implies - session cookies unless they are protected, anything the user can see, anything the user can do. Modern frameworks escape output by default and have made the common cases rare. The remaining ones cluster in specific, recognisable places.
The first is any escape hatch you use to render HTML deliberately - the property or helper whose name usually contains a warning. Rich text from users, content from a CMS, a snippet from a third party. If that content is not sanitised with a proper library before rendering, it is an injection point. Writing your own sanitiser is a bad idea; the edge cases in HTML parsing are numerous and well explored by attackers.
The second is context confusion. Escaping is context-dependent: what is safe inside an HTML element is not safe inside an attribute, a URL, a style block or a script block. A value inserted into an href without validation can be a javascript: URL. A value inserted into inline JavaScript escapes a different set of characters entirely. Frameworks handle the HTML body case well and the others less consistently, so anywhere you are placing data into an attribute or a URL deserves a second look.
The third is the DOM-based variety, which never touches your server at all. Client-side code reads something from the URL or from storage and writes it into the page, and no amount of server-side escaping is involved. This is increasingly the common shape in single-page applications, and it is only findable by looking at what the frontend does with untrusted values.
The control that limits the damage regardless is a Content Security Policy. A header telling the browser which sources of script are permitted means that even if an attacker injects a tag, the browser refuses to execute it. Deploy it in report-only mode first, collect violations for a couple of weeks, fix the legitimate ones, then enforce - and expect the exercise to reveal inline scripts and third-party tags you did not know you had, which is itself worth knowing.
Two smaller things close the gap. Session cookies with HttpOnly cannot be read by injected script, which removes the most valuable prize. And be deliberate about third-party scripts: every analytics tag, chat widget and advertising script executes with full access to your pages, so a compromise of any of those vendors is a compromise of your site. Subresource integrity where the script is static, and a hard look at whether each one earns its access, is the whole of the defence available there.