This article is part of the C# Advent Series check it out for more articles from others in the community
I love C#. I’ve been working with the language since 2001 and still view C# as my favorite and primary programming language, despite growing to love many other languages as well since then. However, this year has been eye-opening for me as I’ve gotten a glimpse into how others learn programming and the problems C# has with new developers.
Learning programming is hard, but learning a language that was introduced 20 years ago and continues to radically grow and change year after year is monumentally harder.
In this article I’ll lay out some of the current problems I see with C# for new developers and talk about my hopes for the future of the language, including some early information on changes Microsoft is looking at including with .NET 6 and a feature request I’m hoping Microsoft will implement in Visual Studio.
It is my hope that this article helps others empathize more with new developers and take new developers into account when designing their own libraries, languages, and tooling. I would also love it if Microsoft entertained some of the suggestions in this article, but my primary desire is for the community to have a bit more empathy for new developers learning in a broad and deep ecosystem.
While it’s normal for experienced developers to feel waves of impostor syndrome throughout their career, it can be harder to remember that feeling of being a brand new developer writing your first few pieces of code.
As a beginner, your primary focus is on learning the syntax and learning to think like a machine, but there are so many ways things can fail and a vast array of things you’ve not even encountered yet.
The “Not All Code Paths Return a Value” compiler error in C# is one that almost all of our students run into as they start making their first methods and playing with conditional logic.
Let’s say you need to write a method to return a string based on whether the number is above, below, or inside of a specified number range. A seasoned programmer would have no problem with this, but new programmers often try an approach that looks something like this:
The core logic here is correct: We have 3 good checks for things that have appropriate boundary conditions and return the right result, but the compiler isn’t happy:
A beginning programmer looks at this CS0161 identifier and it scares them because it’s an alien string from a computer. They see “not all code paths return a value” and think “Did I miss a return statement inside my if statements? Is there an edge case I forgot?” and they’ll validate that any number going into the method should meet one of those 3 scenarios. They think about the logic and not necessarily about the compiler’s perspective.
Beginner programmers like repetition. They like thinking about things in terms of if statements and they’ll repeat the same types of condition checks with slight edits. It’s very normal for people to do things like this as they get used to programming. Only later do they think “I don’t need an if here if I’m returning on the prior lines for the other conditions” because it takes time to become truly familiar with the return statement and its implications.
I have a fix in mind for this error in particular which I’ll get to in a bit.
Just to illustrate that I’m not just picking a random bad compiler error, let’s take a look at another case folks are likely to try at some point:
Here the programmer forgot to assign favoriteNumbers a new list of integers. Most programmers, when confronted with these two lines in close proximity, will spot this issue and be able to correct it. The problem is that these are often less clear with longer methods, additional variables, loops, if statements, etc.
So let’s take a look at the error message Visual Studio offers. The message once again tells us what’s wrong, but most experienced developers forget that reading error messages is an acquired skill. To the novice “unassigned” might not make them think “I need to create a new list and set that list into my variable”.
This has all the right words, but it could be clearer with one more sentence asking if they meant to set the variable to a value when declaring it.
Let’s shift gears a bit to talk about how C# and .NET is awesome. As I mentioned in the opening, I’ve been working with C# since beta 2 back in 2001. You don’t stick with a language that doesn’t adapt and change over time.
.NET has made some phenomenal changes over the past two decades with the major new platforms such as Razor, .NET Core, MVC, WPF, LINQ, Entity Framework, and Microsoft Azure, among other things. As technology continues to change, .NET remains relevant entirely through these efforts.
However, with two decades of progress and history, there is a price to pay, and unfortunately it is the new developers who bear the brunt of it.
As C# has grown over the years, it’s gained a number of incremental improvements in new language features. As new capabilities and keywords get added on, older ways of doing things remain supported to provide easy migration paths. This means that newcomers in tech are likely to see things a number of different ways in looking over existing code and documentation.
This problem is perhaps at its most prevalent with properties. Let’s take a look at all of the different ways of handling properties in C#.
Here we have all the currently supported ways of working with C# properties in one class. As you can see, there’s a lot of valid, but different syntax. Some of these pieces of syntax are matters of preference (expression-bodied members), and some are necessary to support certain scenarios.
A novice programmer needs to be familiar with reading and understanding most of these. Visual Studio will actually automatically insert properties as expression-bodied members when overriding a class, which forces that arrow syntax on new devs who may not be ready for it yet.
C# has a growing number of keywords – particularly when dealing with method parameters and with the concept of a class vs a struct vs an interface vs a tuple vs a record vs an anonymous object.
There’s simply a lot of things that a newcomer needs to be able to read and understand in online help, starter templates, articles, books, and existing code. Not only does a newcomer need to understand how to read these things, but they must internalize guidelines and rules on when to use what type of thing as part of the process of understanding each new thing they know.
Unfortunately, the tooling makes it easy to discover some of the newer and more advanced language features when you don’t necessarily want people to.
Take this error message our students might encounter when running on a slightly older version of C# and working with interfaces for the first time:
Here the student has tried to define an interface containing a method body out of habit. The compiler message (pictured above) is more encouraging towards upgrading C# versions to take advantage of default interface method implementations as opposed to pushing them to double check if they wanted to add a method body to an interface to begin with.
Note: Interestingly, newer versions of C# don’t error on this code at all, making it harder for a student to understand the difference between an abstract class and an interface and what the intent of either one really is.
This is an example of a new language feature making it harder for newcomers to learn core aspects of the language first, which is unfortunately a common trend.
At this point hopefully you have a slightly larger understanding of some of the things that concern me about people learning C# 2 decades into its existence.
Let’s talk more about remedies for these things.
Here are a few things that I think would help people have an easier time getting into C# and feeling confident and competent with the language:
The number one thing I would encourage Microsoft to change would be the way that compiler errors appear to the user when the user hovers over the “red squiggly” in the interface.
Let’s take a look of a simple mock-up of what might appear with the “Not all code paths return a value” error I cited earlier as an example:
Here we have a few changes:
- First, the “red squiggly” occurs at the end of the method instead of at the method title. This helps the developer with this particular error by putting the squiggly closer to the spot of the missing / incorrect code.
- Secondly, the error tooltip is vastly different and includes basic contextual information up at the top, a short beginner-friendly paragraph describing the main cases of the error, and includes examples of bad and fixed code.
- Finally, the error code is still included at the footer, along with a hyperlink to learn more details from the official documentation on this error code.
This type of experience would be optimal for a new developer and would take a certain degree of fear out of the experience while steering them in the direction of a proper fix.
Note: This feature has been officially registered as a feature request. If you believe that this provides value, please upvote it.
These tooltips would be hard to do with many errors, but I’m certain Microsoft has some idea of which compiler errors are the most common errors and could prioritize providing help for those scenarios. Here are a few I’d prioritize myself:
- CS0029 – Cannot implicitly convert X to Y
- CS0103 – X does not exist in the current context
- CS0161 – Not all code paths return a value
- CS0165 – Use of unassigned local variable
- CS1002 – Semicolon expected
- CS1513 – } expected (missing closing scope)
- CS1525 – Invalid expression term. Specifically when using == instead of = for assignment (e.g. int i == 4)
- CS7036 – No argument given that corresponds to the required formal parameter (incorrect method call)
It’s healthy and expected to add more language features to support developer productivity and keep up with the changing nature of programming. The recent rise in popularity of functional programming is a great example of this as it has pulled in a lot more functional-style syntax into newer versions of the C# language.
The problem comes when more complex language features are pushed to new developers still trying to learn the basics.
For example, when a new developer is trying to implement an interface for the first time, C# will helpfully offer to generate members for the user. That’s an awesome feature. However, the implementation is maybe not the best for new users:
Here C# generates a property getter and setter, but prefers the expression-bodied member syntax of the language.
This is actually my preferred syntax for writing a simple property, but this is distracting to show someone brand new to C# and programming in general because there are so many other fundamentals to focus on first before you get to the point where you can learn arrow functions.
One of the most surprising things for me as a new instructor was seeing how much trouble new developers have understanding properties. New programmers are juggling so many different concepts and this idea of a class as a re-usable piece of code with little properties that can be customized takes time to soak in. During this “soaking” period, properties don’t make a lot of sense and, unfortunately, there are far too many ways of writing them at present.
What’s worse, when Visual Studio pushes expression-bodied member properties at a new developer who has only seen the older ways of writing a property (and has potentially not yet seen arrow functions), the beginner may not even realize that they’re looking at a property.
I think what we need is a “Beginner Mode” that simplifies the language of error messages and sets the auto-generated code to prefer older style syntax.
Just like how when you used to set up Visual Studio you would tell it if you had a C#, VB.NET, or Web Development background and it would customize its menus, it’d be nice to indicate that you’re new to programming or new to C# and have Visual Studio minimize the number of things that could distract you from that early learning path.
To be fair to the wonderful team at Microsoft here, there’s definitely new features coming out that you now must opt-in to receive. A notable recent example would be the way C# 8 adjusts how null values are handled by requiring a project-level setting enabled to enable that advanced behavior.
More features need to be handled in similar ways going forward.
We need to seriously think about the growing baggage in terms of number of different keywords and different ways those keywords can be arranged. When we introduce new things, we need to seriously ask ourselves what, if anything, we can take out or deprecate from older versions of the language.
For example, if expression-bodied members are truly designed to replace standard field-based properties with gets and sets, we should say that you should stop using the older way of doing things and provide compiler warnings to guide people away from them. (Note, I’m not saying this change should be made, I’m just using it as an example of adding complexity without removing anything)
Even better, we should provide handy quick-fixes that automatically convert old code to use the new recommended ways of doing things, and let you do that at the file, project, or solution level.
By making explicit best practices clear in our tooling, we remove things that new developers need to care about. Sure they need to care about understanding the old and new way, but they don’t need to worry about deciding when they should use one way over another.
Beginners like and need guide rails because they reduce mental strain and improve comfort as people learn the ropes by focusing on one thing at a time.
The good news is that making .NET Accessible to new developers is a central theme currently under consideration for the .NET 6 release.
Looking into the current theme, it looks like Microsoft is aiming for the following improvements:
- Reducing the strain of legacy features in Visual Studio by hiding things not related to modern development
- Emphasizing task-based learning in the documentation
- Allowing people to work more with VS Code for .NET Development
- Reducing cognitive load needed for using .NET Command line tooling
Overall, I’m elated at this, but I’d still love to see more progress on improving the compiler and runtime errors users see and gearing them towards those brand new to programming.
I strongly support and embrace Microsoft’s approach in prioritizing new developers coming into the language as the language and its tooling continue to grow and evolve and I’d love to see some additional effort on simplifying legacy ways of writing C# code or guiding people towards more modern approaches in a way that’s as friendly as possible to new developers while respectful of existing code.
This article may read like a wish list or an airing of grievances about things that make my job harder as an instructor, but it’s actually not my students I’m worried about. A skilled instructor can serve as a tour guide through an existing language.
My fear is for the folks who want to learn C# in greater depth than their schools teach it, or who are trying to improve themselves on the side. My fear is also that students may struggle and give up due to the daunting learning curve early on when they could have succeeded if they’d had a bit more help.
We need a wider diversity of people coming into tech, and the learning curve is one of the many factors stifling that growth. More people should have a fair shot at learning these technologies, and that means revisiting our tools and documentation to welcome the next generation of developers from all ages, genders, and backgrounds.
I am incredibly proud of .NET and all of its supported languages and associated tools. Microsoft has done a phenomenal job over the past 20 years and I cannot overstate that, despite the improvements I want to see.