What have I learned from working with legacy code?
Legacy code (code you've inherited from someone else, likely without tests) is evil and feared by those who are sentenced to work with it. It sucks the life out of one, doesn't it? Yet it is similar to the "what have the romans ever done for us" scene from Monty Python's Life of Brian - there are a lot of great things to learn from it. I would even consider it a required experience to become a well-rounded software developer. Below is my (by no means complete) list of things I've learned from legacy code.
Humility was a hard lesson - I've been rather confident in my skills and ability, yet I have (and seen others too) break working applications without noticing, looking for bug sources in places where they are logically expected to be yet hiding nowhere even close to that area, that misleading can the code be. I've had to face that by no means can I just rely on my gut feeling, and I've picked up a few nice tricks (e.g.: run the application with a coverage tool attached to ensure proper and quick impact analysis area when you are tasked with enhancing the behavior for a button click - nasty surprises can be avoided!).
Scope discipline is a related lesson - stay focused, and only change the parts of the code that you really need to. You might realize that there is a broken csv parser implementation there, which you can rewrite or replace with a standard library while working on some nearby area of the codebase... Don't change it then - it might have quirks you are not aware of, bugs, which are actually required for the daily use of the software. It is going to take much longer than expected to fix that (and fix/discover the unintended consequences). Just make a note, then finish what you are working on, and then raise it as an improvement. If it ain't broken, don't fix it.
Some of these applications require a long time to start, and have tons of dependencies - unit testing was a cute thing I've learnt during my school days, but at the beginning of my career I didn't practice it for the short, quick projects I've worked on. Working with legacy applications pushed me to learn more about automated testing. Of course, legacy code is rather hard to test, so one learns about disciplined refactoring and other techniques to make them testable and ends up learning about (refactoring to) design patterns. The ability to quickly isolate just the bit you are trying to test and stop there has served me well ever since - perfect is the enemy of the good, so just enough is enough most of the time.
Legacy code usually also means being part of the maintenance phase of the lifecycle (which is much longer than the initial development), so one really starts to feel the pain of deployment and quickly learns to automate as much as possible. I've been lucky enough to have had the on-call support experience, where I learned to value useful log messages with appropriate alerting strategies. Even when the app doesn't have an ops team of its own I find some (friends) to ask about support/monitoring requirements they would have for the new features being developed.
Dealing with the change requests also made me realize that most of the time we just can't plan for the future - changes are never to be made at the planned extension points (aka YAGNI). Just keep the code as small and simple as possible - this also results in faster learning curves for new developers, since they don't need to learn complex architectures. Ron Jeffries' Extreme Programming Adventures in C# shows a great example of just enough design for a real program.
After having waded through some obscure cornet of the application, I always feel that I need to share the knowledge with the rest of the team. On the one hand, if I can save them time, the better, on the other hand, I don't want to become a single point of failure. Thus I value code reviews, post mortems/retrospectives, good documentation/fixlogs - knowledge silos are an antipattern!
So, you'll get familiar with antipatterns, smells, which, when read about in the abstract, sometimes are hard to actually understand. Seeing (and suffering because of) them will make you remember, and it will become second nature. Like intention revealing variables, method, and class names that can be trusted.
In summary: Legacy Code is a great learning opportunity both for technical skills and attitude.