JS Procedural Hangman
Logic

Is the basic logic OK? Any obvious improvements?

Well does the game work? If so then the basic logic must be OK.
Is there room for improvement, yes.
The code is tangled mess of roles and states.

You have 4 functions that manage 7 distinct game states.
There is some repeated code (win and lose).
You have magic values in the code (text prompts and displays)
The whole thing runs in a continuous blocking state which is VERY bad for browser based code (you can not exit until end game)
There is no checking for repeated character guesses, or valid character guesses.
You randomly pick words, not checking if a new game is using an already used word.

Example
The example shows some alternative approaches to creating game like state machines.
Note that I do not use the console, prompt, or confirm, thus the game needs an additional wait state to wait for input.
Nor do I vet input for repeats, or bad characters.
update I have fixed the inplay state to only allow start when not inplay
Some points.

Uses finite state model to control the game.
Settings out of the code and grouped in one place to allow easy changes.
Encapsulate the game in a singleton (to keep the global scope clean)
Though I do not use prompt and confirm, the state changes use setTimeout to change between states, this stops the game from blocking the page.
States are pedantic but there roles are clear and without ambiguity.



"use strict";  // Always add this to the top of your code.


/* Start game is external state request */
document.getElementById("play").addEventListener("click", ()=&gt; {
    hangman.state = "start";
})


const hangman = (()=&gt; {
    const settings = {
        limit : 4,
        words : ["fish","cat","chicken","freedom","turnip"],
        text : {
            start : "Guess one letter at a time to reveal the secret word.",
            prompt : "Guess a letter. You have ## remaining guesses", // ## is replaced with remaining guesses
            good : "Well done.",
            bad : "That letter in not in the secret word.",
            win : "Yay, you guessed it!",
            lose : "You have used all your guesses. Game Over.",
            again : "Play again Y/[N] ",
            gameOver : "You have used up all the words. GAME OVER!",
            exit : "Bye!",
        }
    };
    
    const display = text =&gt; log(settings.text[text] ? settings.text[text] : text);
    var word, player, inplay, wrongGuesses;
    

    
    function start() {
        if(settings.words.length &gt; 0){
            word = settings.words.splice(Math.random() * settings.words.length | 0, 1)[0];
            player = "";
            wrongGuesses = 0;
            display("start");
            display("Cheat &gt;&gt;&gt; " + word + " &lt;&lt;&lt;&lt;");
            return states.guess;
        }
        return states.gameOver;
    }
    
    function getPlayerGuess() {
        const gotKey = (key) =&gt; {
          const guess = key.toLowerCase();
          player += guess;
          game.state = word.indexOf( guess ) === -1 ? states.badGuess : states.goodGuess;
        }
        var remaining = settings.limit - wrongGuesses;
        const guess = prompt(settings.text.prompt.replace("##", remaining), gotKey);
        return states.wait;
    }    

    function checkPlayer(){
        var wordDisplay = ""; 
        for (const char of word) { wordDisplay += player.indexOf(char) &gt; -1 ? char : "_" }
        display(wordDisplay);
        return wordDisplay === word ? states.win : states.guess;
    }
    
    const states = { // All game states named
        start : 1,
        guess : 2,
        badGuess : 3,
        goodGuess : 4,
        win : 5,
        lose : 6,
        playAgain : 7,
        gameOver : 8,
        wait : 9,
    };

    const game = {  // assign property game.state to change state
        set state(value) { 
            if(! inplay){
                if(value === "start") {
                    value = states.start;
                } else {
                    return;
                }
            }
            inplay = true; // prevents game restart while in play     
            var nextState; // if left undefined then game ends
            switch(value) {
                case states.wait:
                   return;
                case states.playAgain:
                    confirm(settings.text.again, result =&gt; {
                       game.state = result ? states.start : states.exit;
                    }); 
                    nextState = states.wait;
                    break;
                case states.start:
                    nextState = start();
                    break;
                case states.guess:
                    nextState = getPlayerGuess();
                    break;
                case states.goodGuess:
                    display("good");
                    nextState = checkPlayer();
                    break;
                case states.badGuess:
                    display("bad")
                    wrongGuesses += 1;
                    nextState = wrongGuesses === settings.limit ? states.lose : states.guess;
                    break;
                case states.win:
                    display("win");
                    nextState = states.playAgain;
                    break;                
                case states.lose:
                    display("lose");
                    nextState = states.playAgain;
                    break;
                case states.gameOver:
                    display("gameOver");   
                    break;
                default :
                    return;
            }
            if (nextState) { setTimeout(() =&gt; game.state = nextState, 0) }
            else { 
                display("exit");
                inplay = false; 
            }
        }
    };
    return game;
})();






















/*========================================================================*/
/*========== unrelated code to handle input and display ==================*/
var prompting;
var keyVal;
document.addEventListener("keyup",e=&gt;{
    if(prompting){
        keyVal = e.key;
        prompting = false;
    }
})
function prompt(text,cb){
    var ticker = 0;
    const line = log(text );
    prompting = true;
    function tick(){
        if(prompting){
            line.textContent = text + (ticker++ &amp; 1 ? "?" : ".");
            setTimeout(tick, 250);
        }else{
            line.textContent = text + "! " + keyVal;
            cb(keyVal[0]);
        }
    }
    tick();
}
function confirm(text, cb){
   prompt(text,(key)=&gt;{ cb(key.toLowerCase() === "y") });
}
function log(text){
    var line;
    if(textContainer.children.length &gt; 7){ line = textContainer.removeChild(textContainer.querySelector(".text")) }
    else{ line = Object.assign(document.createElement("div"),{className : "text"}) }
    textContainer.appendChild(line)
    line.textContent = text;
    return line;
}
body {
   background : black;
   font-size : 16px;
   color : #0f0;
.text {


}
&lt;input type="button" id="play" value="play hangman"&gt;
&lt;code id="textContainer"&gt;
&lt;/code&gt;



Style

Any details or matters of style I need to address?

The style is generally good. Adding spaces after opening and before closing parentheses is a little unusual console.log( solutionSoFar.join( &quot; &quot; ) ) it is by no means bad. The most important aspect of code style is that you use it consistently, which you have done.
Comments should be kept to a minimum, they constitute un-vetted noise. None of your comments are needed, the code is self explanatory.
Variable declaration type

var vs const vs let - ... My basic approach was &quot;If it needs to be global, use var, if it's not going to change, use const and if it is going to change but is not global, use let.&quot;

const
OK first const it will be the most used type of variable declaration in any good code. You used it correctly for words but missed using it for limit.
For solutionSoFar which holds an array you assign a new array each time you need. There is nothing wrong with that. But it could also have been a const and it would be declared as const solutionSoFar = []; and solutionSoFar.length = secretWord.length;' followed by solutionSoFar.fill(&quot;_&quot;);`
Doing it this way lets you keep additional references to the same array, and reduces the overall memory management overhead (in this case negligible overhead)
var V let
Using either in global scope makes no difference apart from when you can access them.
var is function scope and hoisted. The global scope can be thought of as the global function and var has scope to everything within that call. It is also hoisted to the top of the code and thus accessible at any time (however it may not yet contain a value).
let is block scoped and is not hoisted. Again the global scope like a function is contained in a block and thus let is accessible from anywhere but only after its been declared or it will throw an error (Warning let and const can not be accessed before they have been declared) .
Which to use?
The argument that block scope reduces bugs is non sequitur.
What you use is up to you. But var for global and let for functions makes no sense. I use the rule that if declared in function scope (and not a const) it should be a var
eg
function foo() {
    var funcScope;  // this is correct for function scope
    let blockScope; // This could / (should in my book) be function scope

    if (true) {
        var blockVar; // this is function scoped and should be at the top
                      // of the function
        blockScope = 0;    // Reference error blockScope is not defined
                           // The next line recreates the variable for this
                           // scope putting the name in a temporary dead zone
                           // This is a run time error and will not be 
                           // thrown when parsing even in strict mode.
                           
        let blockScope; // this is correct and is a new instance, not the
                        // same as the previouse declared blockScope
    }
}