Picking a strong AES key is the easy part. Deciding where that key actually lives once your application is running is where most of the real mistakes happen, and it is a question that changes depending on whether you are protecting data in a browser, a server process, or a mobile app. There is no single right answer, only a set of tradeoffs, so it helps to walk through the common options and be honest about what each one actually protects against.
Both of these are plain text storage, readable by any JavaScript running on the page, including a malicious script injected through an XSS vulnerability. If your application has even one unsanitized input rendered back to the page, an attacker who exploits it can read everything in localStorage in one line of code. Neither is designed for secrets. Use them for UI preferences or non-sensitive cached data, never for an encryption key.
On a server, environment variables are a reasonable place for a key, since they are not exposed to the outside world by default and most deployment platforms keep them out of your source control. The catch is that anything prefixed to be exposed to client-side code (in Next.js, anything starting with NEXT_PUBLIC_) gets bundled straight into the JavaScript your browser downloads, which means it is not a secret anymore the moment it ships. An encryption key should never carry that prefix. If a server process needs the key, keep it server-only and never pass it down to the client.
Services like AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, or your hosting provider's built-in equivalent exist specifically for this problem. They add access control (only the services that need the key can fetch it), audit logging (you can see who or what retrieved a secret and when), and rotation support (issuing a new key without redeploying your entire application). For anything processing real user data, this is the option worth the setup cost. Environment variables are a reasonable stepping stone for a small project, but they do not give you any of that visibility once something goes wrong.
There is one case where none of the above applies: when a key only needs to exist for the length of a single operation and never needs to be retrieved again. If you are encrypting a value in the browser, handing the ciphertext to someone else, and never need that key again on this device, there is nothing wrong with generating it in memory, using it once, and letting it disappear when the page closes. The problem only starts when you try to persist that kind of key somewhere for reuse, since persistence is exactly what turns a throwaway value into a target worth stealing.
The Encrypt / Decrypt tool on this site is a good example of the throwaway case done right: it generates keys and IVs in your browser using the Web Crypto API, runs the encryption in a Web Worker, and never writes anything to localStorage or a server, because nothing you type into it needs to be remembered after you close the tab.