I have taught hundreds of beginner developers through CTROTECH over the past few years. I expected to help them learn. I did not expect they would make me a better engineer.
Here is the pattern I noticed: every time a student struggled with something, it revealed an assumption I had been making in my own code. Fixing those assumptions made my code better.
This article is about five specific patterns I observed across hundreds of students and how they changed the way I write software.
Pattern 1: Beginners Struggle with Implicit Behavior
When students first learn JavaScript, implicit returns cause a lot of confusion.
const double = (x) => x * 2
The confusion comes from implicit behavior, not from the arrow function syntax itself. The function returns without a return keyword. For an experienced developer, this is a convenience. For a beginner, it is magic.
Code that relies on implicit behavior is harder to explain. And if it is harder to explain, it is harder for future readers to understand.
This shifted how I look at my own code.
// Hard to explain — implicit chaining:
const processUsers = (users) => users
.filter((u) => u.active)
.map((u) => ({ id: u.id, name: u.name.toUpperCase() }))
// Easier to explain — explicit steps:
function processUsers(users) {
const activeUsers = users.filter(user => user.active)
const formattedUsers = activeUsers.map(user => ({
id: user.id,
name: user.name.toUpperCase()
}))
return formattedUsers
}
The second version is more lines. But each step is explicit. You can point to a variable and explain what it contains. The intermediate state is visible instead of hidden.
I am not saying you should never chain methods. I am saying that when I catch myself writing something I would struggle to explain to a student, I stop and consider whether it needs to be that way.
Pattern 2: Variable Names Reveal Clarity of Thinking
Beginners use bad variable names because they have not yet learned that naming is part of engineering. But experienced developers do it too, just with more sophisticated bad names.
The names I see most often in beginner code that also show up in production codebases:
-
data— what kind of data? -
temp— temporary for how long? -
result— result of what? -
items— what items?
Teaching forced me to be specific. When a student asks "what does this variable hold?" and I have to trace through five lines to answer, the naming is the problem.
// Before — every name requires context:
const d = await getData()
const r = d.filter(x => x.s > 5)
return r.map(x => ({ ...x, label: x.s.toString() }))
// After — each name tells a story:
const products = await fetchProductCatalog()
const highStockProducts = products.filter(
product => product.stockCount > 5
)
return highStockProducts.map(product => ({
...product,
label: String(product.stockCount)
}))
The second version takes longer to type. It takes less time to read. And reading is what developers spend most of their time doing.
Pattern 3: Functions That Do Too Much
Beginners write long functions because breaking things into smaller pieces requires understanding the boundaries. Experienced developers do it because they are moving fast and will refactor later.
I started using a simple test: if I cannot explain what a function does in one sentence, it is doing too much.
// One sentence? "This processes orders." — Too vague.
function processOrders(orders, users, inventory) {
const validOrders = orders.filter(o => o.status === 'pending')
const enrichedOrders = validOrders.map(o => ({
...o,
user: users.find(u => u.id === o.userId),
available: inventory[o.productId] >= o.quantity
}))
const confirmedOrders = enrichedOrders.filter(o => o.available)
confirmedOrders.forEach(o => {
inventory[o.productId] -= o.quantity
sendConfirmationEmail(o.user.email, o)
})
return confirmedOrders
}
This function filters, enriches, checks inventory, updates stock, and sends emails. Explaining it requires walking through each step.
// One sentence each:
function getPendingOrders(orders) {
return orders.filter(order => order.status === 'pending')
}
function enrichWithUserData(orders, users) {
return orders.map(order => ({
...order,
user: users.find(u => u.id === order.userId)
}))
}
function checkAvailability(orders, inventory) {
return orders.map(order => ({
...order,
isAvailable: inventory[order.productId] >= order.quantity
}))
}
function confirmOrders(orders, inventory) {
orders.forEach(order => {
inventory[order.productId] -= order.quantity
sendConfirmationEmail(order.user.email, order)
})
return orders
}
More functions. More files. But each one has one responsibility and one explanation. When something breaks, you know where to look without reading the entire pipeline.
Pattern 4: Error Messages Are a Form of Teaching
Beginners stop reading when an error message is unhelpful. They do not lack curiosity. They lack context. An error that says "Something went wrong" gives them nothing to work with.
I realized my APIs were doing the same thing to their users.
// Bad — tells you nothing useful:
app.post('/api/users', async (req, res) => {
try {
const user = await createUser(req.body)
res.json(user)
} catch {
res.status(500).json({ error: 'Something went wrong' })
}
})
// Better — tells you what happened and what to do:
app.post('/api/users', async (req, res) => {
try {
const user = await createUser(req.body)
res.json(user)
} catch (error) {
if (error.code === 'EMAIL_EXISTS') {
return res.status(409).json({
error: 'A user with this email already exists.',
suggestion: 'Try logging in instead.'
})
}
console.error('User creation failed:', error)
res.status(500).json({
error: 'Could not create user.',
suggestion: 'Please try again later or contact support.'
})
}
})
An error message is a moment of teaching. If the user or the next developer cannot understand what went wrong and what to do next, the error has failed its purpose.
Pattern 5: Code That Resists Copy-Paste Without Understanding
Beginners copy code. Every teacher knows this. But copying without understanding creates hidden problems.
I began making the important decisions visible in my code.
// Easy to copy, easy to misuse:
function saveUser(data) {
return db.insert(users).values(data)
}
// Harder to copy, harder to misuse:
function saveUser({ email, name, role }) {
if (!isValidEmail(email)) {
throw new Error('Invalid email format')
}
if (!['admin', 'user', 'viewer'].includes(role)) {
throw new Error('Role must be admin, user, or viewer')
}
return db.insert(users).values({
email: email.toLowerCase(),
name: name.trim(),
role,
createdAt: new Date()
})
}
The second function makes its assumptions explicit. You cannot call it without knowing what it expects. It validates inputs, transforms data, and fails early. A developer looking at this function knows exactly what is required and what will happen.
What This Changed About My Engineering
Here is the framework I use now when reviewing my own code.
I now check my code against a short framework. Make every state change visible. Name variables for the person who will read them next year, not for the person writing them now. Keep each function explainable in one sentence. Treat every error message as information a developer will rely on when something breaks. And reveal assumptions before they cause bugs: validate early, fail clearly.
None of these rules are original. Teaching did not invent them. Teaching forced me to take them seriously.
What I Am Still Figuring Out
- How to balance conciseness with explicitness in different codebases
- When implicit patterns like chaining are worth the cognitive cost
- How AI-assisted development changes these patterns
I do not have settled answers. But the question I keep returning to is the same one: can I explain this to a beginner? If not, the problem is probably my code.
Top comments (0)