>Doing logic in a constructor is trouble in any language.

This remark is often made, as is the similar comment that throwing exceptions from a constructor is an antipattern. However, it's often said without saying why it should be the case, and I get the feeling that sometimes it is parroted without understanding. (OP: this is a general comment that is not directed at you). For that reason, it took me a while to figure out the motivation for the idea through experience (read: making mistakes and realizing them in retrospect).

So, why is it bad to have logic in a constructor, or have a constructor that can throw an exception?

There is nothing, necessarily, inherently wrong with doing so, at the level of the specific object; the concern is at a higher level. The idea is, rather, that doing so is an indicator of more widespread design problems in the overall application. Let's look at an example.

Suppose that I have an object that is supposed to serve as a container for data that is retrieved from a database - a User object, perhaps, which is supposed to hold, say, a user's username, GitHub profile link, and other such things. Suppose that I define my User class so that its constructor queries my database for the its data and throws a SQLException if the query fails.

A fundamental principle of good software design is that of decoupling. Decoupling essentially refers to the idea says that a component of a system should not know more than it needs to know in order fulfill its role, nor interact with more components of the system than it absolutely needs to.

If our object is supposed to hold data that is needed by other parts of the application, it should not matter to it where the data comes from or how it gets there; that is, if I have a User class that can throw a SQLException while being constructed, then the User class simply knows too much - a database connection error has nothing to do with a User, and so if User's constructor does database retrieval in its constructor, then it hasn't been decoupled from the data layer of the application.

To illustrate why this principle exists, suppose we have to switch our users data store from Postgres to Mongo; the User class should not have to be modified. Having User do query logic or be able to throw a SQLException in its constructor means that it hasn't been decoupled from the data layer. This is why people say, broadly, that doing logic or throwing exceptions from constructors is "bad" - it's not bad in and of itself, but it is bad in the sense that it indicates that the overall application does not have good decoupling, which can be a long term problem; furthermore, the severity of the potential problem increases probably exponentially with the overall size of the application. Imagine having dozens of data object that each do database queries in their constructor - switching databases means you'll have to modify every single one of these classes!

Just as important is commentary on how to avoid getting into this situation. Let's use the same example - how do we decouple our User class, which does a database query in its constructor, from our data layer?

There are numerous ways, but the general idea is to define a component of the application whose job is to connect to the database and read data out, along with a separate component of the layer that contains abstractions like User which are constructed from this retrieved data. Concretely, in our User example, we might first define something like a UserLocator class which takes in a database connection and that has a method which takes a User ID and returns some corresponding data from the database, and then a User class whose constructor takes that data and pulls out what it needs. Later, were we to switch underlying databases, we would only change UserLocator to query a different database but have it still return the same data format that our new, decoupled User class' constructor expects. This way, in the long term, our application is easier to adapt to changes because we have good decoupling. Applying this principle (and others) across an entire codebase can end up being the difference between spending 90% of your time maintaining legacy code versis spending 10% of your time updating existing code to meet new requirements.

Hopefully someone finds this useful. I wish I had learned principles like these earlier in my career; this sort of thing is definitely not taught in computer science programs (nor, arguably, should it be - but many people get hired out of such programs into places where knowledge of design principles is very important).