Q:

Why does .then() work without a promise in JavaScript?

Why does calling the second function .then(notPromise) still pass the argument to the third function .then(promiseC) even though notPromise() is just a regular function?
I thought only promises can be used with .then() but somehow it still executes (and passes the arguments) properly.

promiseA()
    .then(notPromise)
    .then(promiseC);


function promiseA() {
    return new Promise(function (resolve, reject) {
        const string = "a";
        resolve(string);
    });
}


function notPromise(string) {
    const nextString = "b"
    const finalString = string + nextString;

    return finalString;
}


function promiseC(string) {
    return new Promise(function (resolve, reject) {
        const nextString = "c";

        const finalString = string + nextString;

        alert(finalString);
        resolve(finalString);
    });
}

A:

The then() method returns a Promise.
  See docs.

A promise has a handler method. Once a Promise is fulfilled or rejected, the respective handler function will be called asynchronously. The behavior of the handler function follows a specific set of rules as stated here.
Let's go over them one by one. Here is the code we will inspect side by side. Its nothing special, just a chain of promises returning values.
let sequence = new Promise(function (resolve) {
  console.log('Say 1')
  resolve(1)
})

sequence
  .then(() => {
    console.log('Say 2')
    return 2
  })
  .then(() => {
    console.log('Say 3')
  })
  .then(() => {
    console.log('Say 4')
    return Promise.resolve(4)
  })
  .then(() => {
    return new Promise(function (resolve) {
      console.log('Say 5')
      setTimeout(() => { resolve(5) }, 1000)
    })
  })

returns a value, the promise returned by then gets resolved with the returned value as its value;

In code, this is Say 2, and your original question. When a value is returned, then() returns a Promise which is resolved with the value your returned.

doesn't return anything, the promise returned by then gets resolved with an undefined value;

same as above.

throws an error, the promise returned by then gets rejected with the thrown error as its value;

same as above, except now then() returns a Promise which is rejected with your error.

returns an already resolved promise, the promise returned by then gets resolved with that promise's value as its value;

In code this is Say 4, where the promise has already been resolved. So now then() returns a Promise which is resolved with the value 4.

returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value;

same as above, except it now rejects.

returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the value of the promise returned by then will be the same as the value of the promise returned by the handler.

In code, this is Say 5. If you return a promise which has not been resolved yet, then() will return a Promise with the results of your promise i.e. 5.
One thing to note, that I also actually learned recently (suggested by @Bergi in comments) was that the then() method always constructs and returns a new Promise before the chain callbacks have even started to execute. The callbacks that you pass to then() simply tells the promise the value/error that the promise should resolve/reject with.
In summary, that is why then() chaining works even when you don't specifically return a new Promise - because then() method always constructs a new promise behind the scenes and rejects/resolves that promise with the value you returned. The most complex case in above scenarios is when you return a Promise in your callback, in which case your callback promise's results are passed to the then() promise.

