This is probably not really for first-few-months beginners...
- I did not yet realize how different 1-month personal projects are from large-scale projects made by big teams. Suddenly you don't know all of the code anymore, you have to make assumptions about what other code does, have to follow conventions, the percentage time spend finding bugs goes way up and all timescales in general increase by an order of magnitude.
- I thought type systems are useless, or are only useful for performance. In reality, they help rule out a lot of bugs at compile time, which is absolutely where you want your bugs ruled out, and they are documentation that doesn't get outdated.
- I didn't try to fail as fast as possible. It would seem kind of convenient if the program kept running if I'd forget to pass an argument, or would get null for a a non-existent hashmap key, or when dividing by zero. At least the program was still running, right? Well, it shouldn't be. A clear error is much better than a wrong/undefined result in most cases. If you're unsure about something, make it fail loudly until you are sure. In development, you'll want to move all your errors in this direction: continue with wrong result -> fail with stacktrace -> unit test failure -> compile time error.
- I almost always underestimated the time needed for anything (I kind of still do). I don't know if that's specific for programming. But it takes a lot of experience to be able to make any kind of reliable time estimate.
One day, you will wish your problem were as immediately apparent and easy to find as a missing semicolon...
As an example of the third point:
- "Fail with stacktrace" might happen in many cases where you're solving a bug and don't want it to come back. Lets say your method constructs an SQL query, but when using it with some combination of patameters, you find that you forgot a space and your database rejects it. You then write a unit test for various parameter combinations, which causes this and any future SQL syntax errors: "unit test failure" reached. (Which should in turn prevent you from merging the feature into the main code).
- "Unit test failure" could be the above case, which might become a compile-time error by using a type-safe query builder. Or you might be getting a null-pointer, so you switch to a non-nullable type (in e.g. Kotlin, Swift, variables can be nullable or non-nullable), or you use Java's crappy approximation
- As an example of going straight from "continue with wrong result" to "compile time error", and to illustrate my second point, lets say you might make the mistake of comparing
Customer.id, which does not make sense because they're different tables. Well, if they're not compatible, they're not really the same type of thing, are they? But chances are that the are both integers in the code. So instead you convert them to
Id<Customer>respectively. This won't be possible in a dynamic language, and it will only help a little in e.g. Java that implements
.equals(Object)by default. But in e.g. Rust, you just don't make
Id<Customer>comparable, and it will simply fail to compile if you ever make the mistake. More info.
I know this is not really beginner level, but it took me long to realize, so I want to save you a year or two!