Error Handling in JavaScript: Try, Catch, Finally

Error-Handling Flow 1

Introduction

Imagine this: You’ve just launched your shiny new web app. Users are loving the smooth UI… until one clicks a button and the entire page freezes with a cryptic red error in the console. Your whatsapp starts blowing up with angry messages.

That’s the nightmare every developer has lived through.

The good news? JavaScript gives you a superhero cape called Error Handling — specifically the try, catch, and finally blocks. With these tools, you can turn crashes into graceful recoveries, make debugging easier, and keep your users happy even when things go wrong.

In this blog, we’ll break it all down step by step, with real-world examples, so you can start writing bulletproof code today.

What Are Errors in JavaScript?

JavaScript errors are unexpected events that break the normal flow of your program. There are two main types:

  • Syntactical errors (caught before the code even runs like during compilation — think missing brackets or typos)
  • Runtime errors (the sneaky ones that happen while the code is executing)

Let’s look at some classic ones that crash apps in the wild:

// Example 1: ReferenceError
console.log(nonExistentVariable);  // Oops! Variable doesn't exist

// Example 2: TypeError
const num = 42;
num.toUpperCase();  // Numbers don't have toUpperCase()

// Example 3: RangeError (common in loops or APIs)
new Array(-1);  // Negative array length? Nope.

Without handling, these errors stop execution immediately. Your app crashes, users see a blank screen or broken feature, and you’re left hunting down through console-logs at 3AM.

Using try and catch blocks

This is where the error-handling begins. The try...catch construct lets you attempt risky code and catch any errors that occur.

Basic syntax:

try {
  // Code that might throw an error
  riskyOperation();
} catch (error) {
  // What to do when something goes wrong
  console.error("Something went wrong:", error.message);
  // Show user-friendly message instead of crashing
  showErrorMessageToUser("We couldn't load your data. Please try again later.");
}

Real-world example with graceful failure:

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error("User not found");
    const data = await response.json();
    renderUserProfile(data);
  } catch (error) {
    console.error("Fetch failed:", error);
    // Graceful failure in action
    displayFallbackUI("Couldn't load profile. Showing demo data instead.");
  }
}

Graceful failure means the app doesn’t die — it recovers intelligently. Users see a polite message or placeholder content instead of a broken page. Your app stays alive, and you keep your users on the page.

Pro tip for debugging:

Inside the catch block, you get the full error object with stack trace, message, and line number which is very help.

The finally Block

Sometimes you need to end up with something no matter what happens — whether the code succeeded or failed. That’s exactly what finally is for.

Syntax:

try {
  // risky code
} catch (error) {
  // handle error
} finally {
  // This ALWAYS runs whether there's any error or not
  console.log("Cleanup complete");
}

Classic use case: Closing database connections, hiding loading spinners, or resetting timers.

Error-Handling Flow 1

Throwing Custom Errors

Sometimes the built-in errors aren’t descriptive enough. You can throw errors using the throw keyword.

function validateAge(age) {
  if (typeof age !== "number" || age < 0) {
    throw new Error("Age must be a positive number");
  }
  if (age > 120) {
    throw new RangeError("A Human body can't live more than 120 years!!");
  }
}

// Usage
try {
  validateAge(-5);
} catch (error) {
  console.error(error.name, error.message); // Error: Age must be a positive number
}

Even better: Create custom error classes for more control by inheriting from Error class.

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
    this.statusCode = 400;
  }
}

throw new ValidationError("Invalid email format");

Custom errors make debugging faster because you instantly know what went wrong, where and why.

Why Error Handling Matters

Let’s be honest — writing perfect code is impossible keeping in mind about every possible circumstances! But handling errors gracefully separates good developers from great ones.

Here’s why it’s non-negotiable:

  • Better User Experience: Users never see scary technical errors. They get friendly messages or seamless fallbacks.
  • Graceful Failure: Your app stays functional even when external services (APIs, databases) go down.
  • Easier Debugging: Centralized error logging + stack traces = bugs fixed in minutes instead of hours.
  • Production Safety: Prevent one small error from taking down your entire application.
  • Professional Code: It shows you care about reliability and user trust.

Error Handling flow 2

Conclusion: Make Your Code Bulletproof

Error handling isn’t just a nice-to-have feature — it’s the foundation of reliable, user-friendly JavaScript applications.

By mastering try, catch, and finally (and knowing when to throw custom errors), you transform potential disasters into controlled, graceful experiences. Your users stay happy, your debugging sessions become shorter, and your code becomes something you can truly be proud of.

Next step: Open your current project and wrap at least one risky operation in a try...catch...finally today. You’ll immediately feel the difference.

Happy coding — and may your consoles stay green!

Your AI Agents Are Exploring Blind. Here’s How to Give Them a Map.

Your AI Agents Are Exploring Blind. Here’s How to Give Them a Map.

Draft — queued after ai-autonomy-dod

Every session, Claude reads the same files. Codex re-discovers the same plugin pattern. Gemini asks the same questions about folder structure.

The agents aren’t slow. They’re starting from zero every time.

The missing piece isn’t a smarter model. It’s a standing index.

The Exploration Tax

In a multi-agent workflow, every agent pays an exploration tax at the start of every session. Before it can do anything useful, it has to orient itself: Where does the plugin system live? What naming conventions apply? Which files are subtree-managed and off-limits? What decisions have already been made?

In a small codebase, this is annoying. In a large one — or one with multiple agents running in parallel — it’s where tokens go to die.

The standard fix is a CLAUDE.md or AGENTS.md that describes the codebase in prose. That helps. But prose is passive. An agent reads it and still has to map it to the files it’s about to touch. It’s a README, not a map.

What an Index Actually Is

An index is different from documentation.

Documentation explains. An index locates.

What I want an agent to start with isn’t a paragraph about the plugin system — it’s: “Public functions go in scripts/plugins/. No underscore prefix. The dispatcher in scripts/k3d-manager loads them lazily. Here are the current plugins and their public functions.”

That’s navigable. An agent can read that and go directly to the right file, with the right pattern, without exploration.

The insight behind using Cline as an indexer: Cline has persistent memory and can maintain a structured understanding of a codebase across sessions. Instead of using it as a code generator, you use it as a dedicated reconnaissance agent — one whose job is to read the repo, build a structured map, and keep it current.

Other agents consume the map. They don’t re-explore.

What the Index Contains

A useful codebase index has three layers:

Structure — where things live and why:

scripts/
  k3d-manager       # dispatcher — maps function names to plugin files
  lib/              # core libraries; subtree-managed via lib-foundation
  plugins/          # lazy-loaded modules; one file per tool
  tests/            # BATS suites; pure logic, no cluster mocks

Contracts — the rules agents must follow, encoded as facts not advice:

- New plugins: scripts/plugins/<name>.sh
- Public functions: no underscore prefix
- Private functions: _ prefix
- Privileged commands: _run_command --prefer-sudo, never bare sudo
- if-count limit: ≤ 8 per function; see etc/agent/if-count-allowlist for exceptions
- Subtree-managed: scripts/lib/foundation/ — edit upstream only

State — what’s in-flight, what’s decided, what’s blocked:

Active branch: k3d-manager-v0.9.17
Current task: _antigravity_ensure_acg_session
Blocked on: lib-foundation v0.3.14 (5 fixes pending)

The third layer is what memory-bank/activeContext.md already does. The first two are what Cline can generate and maintain automatically.

The Workflow

The pattern:

  1. Cline indexes once — reads the full codebase, generates docs/index/structure.md, docs/index/contracts.md. Checks them in.
  2. Cline updates on change — when files move or patterns change, Cline updates the index. This is its recurring job, not a human’s.
  3. Other agents start with the index — Claude, Codex, Gemini get the index as part of their context window. They skip exploration. Their first action is implementation.

The result: agents start oriented. They don’t re-discover the plugin pattern on commit seven. The exploration tax disappears.

What This Changes

Three things shift when you add a standing index:

Token efficiency — agents spend their context window on the task, not the codebase. In a 200k-token window, that’s not trivial.

Consistency — every agent starts from the same map. Codex doesn’t invent a naming convention that Claude doesn’t know about. They’re both reading the same contracts.

Handoff clarity — when you switch agents mid-task, the new agent picks up where the last one left off. The index tells it what’s been decided. The memory-bank tells it what’s in-flight. No re-briefing required.

The Deeper Pattern

Multi-agent workflows have a shared-context problem. Each model has its own context window. Each session starts fresh. The codebase is the only thing they all share — but it’s not structured as context. It’s structured as code.

An index is the bridge. It takes the implicit structure of the repo and makes it explicit, navigable, and consumable by any agent in any session.

The agents don’t get smarter. They just stop starting blind.

The codebase is the shared context. The index is the key.

Ansible Certification vs DevOps Certifications: Key Differences and Career Guide

Choosing between Ansible certification and other DevOps certifications can be confusing for IT professionals entering the automation and cloud ecosystem. Ansible certification mainly focuses on automation and configuration management, helping professionals automate tasks like server setup, application deployment, and infrastructure management using simple YAML playbooks. It is considered beginner-friendly and ideal for those starting their DevOps journey.

On the other hand, DevOps certifications cover a broader range of tools and practices, including cloud platforms, containerization, CI/CD pipelines, and infrastructure automation using tools like AWS, Docker, Kubernetes, and Jenkins. This guide explains the key differences, learning curve, and career opportunities to help professionals choose the right path based on their goals in cloud computing and automation.

Learn More:

I built an AI tool for incident investigation (looking for honest feedback)

Hey everyone 👋

Over the past couple of weeks, I’ve been building a side project called Opsrift.

It started from a pretty simple frustration:postmortems, handovers, and incident documentation take way too much time — and most of it is repetitive.

But while building it, I realized something more interesting:

The real problem isn’t writing postmortems.It’s understanding what actually happened during an incident.

So I ended up going a bit further than just a generator.

What Opsrift does right now

The platform is focused on incident workflows — mostly for people working in SRE, support, or operations.

Right now it includes:

  1. Postmortem generator

Takes incident data and generates structured postmortems in seconds.

  1. Handover generator

Useful for shift-based teams — turns messy updates into clean handovers.

  1. Runbook generator

Creates structured runbooks based on incident patterns or inputs.

  1. Incident Investigator (main focus)

This is the part I’m most interested in:

Pulls data from tools like Jira, PagerDuty, and Opsgenie

Correlates it with deployments from GitHub

Tries to reconstruct what actually happened (timeline, possible causes, etc.)

The goal is to reduce the time spent jumping between tools during investigations.

  1. Status page

Basic external communication for incidents.

Integrations

Current integrations:

Jira

PagerDuty

Opsgenie

GitHub

Slack

Confluence

Still early — some of these are rough.

What it’s NOT (yet)

I want to be upfront:

It’s not a replacement for your incident management tools

It’s not perfect at root cause analysis

It’s not “production-grade” in every edge case

Right now it’s closer to:

an AI layer on top of your existing tools to speed up investigation and documentation

Known issues

To save you time:

GitHub login ❌ (bugged right now)

Slack login ❌ (also bugged)

👉 You can still use:

Google login

Email/password signup

Fixing these next.

What I’m trying to figure out

This is where I’d really appreciate help.

I’m trying to validate a few things:

Does the Incident Investigator actually help or is it just “nice to have”?

Are the outputs accurate enough to be trusted?

Would you use something like this in real workflows?

What’s missing for it to be genuinely useful?

Where I want to take this

Longer term, I’m thinking about moving beyond just generating outputs and more into:

detecting patterns across incidents

identifying unstable services

highlighting teams with high escalation rates

correlating deployments with incidents automatically

Basically:

turning incident data into something you can actually act on

If you want to try it

👉 https://opsrift.com

No pressure — even quick feedback is super helpful.

Final note

I’ve worked in NOC/SOC and incident-heavy environments, so this is very much a “scratch your own itch” project.

That said, I’m aware tools like this can easily become:

too generic

inaccurate

or just another dashboard nobody uses

So I’d rather get honest feedback early.

Even if it’s:

“this doesn’t solve anything for me”

That’s useful.

Thanks in advance 🙌

Scaling DevOps Culture: From Improvised Scripts to Platform Engineering

The collection of scripts, manual configurations, and unwritten rules that helped your company get started will eventually begin to hold you back. What once felt improvised and efficient becomes slow and fragile as the team grows. This is a predictable breaking point in an organization’s DevOps culture. The systems that helped a team of ten people move quickly now create friction for a team of fifty. Operational work spreads out, slowing down feature development until you are spending more time dealing with custom deploy logic than writing code.

The costs of “good enough” automation

Early automation is usually just about being practical. You write a script that solves the immediate problem, set up the CI job that gets the build running, and move on. This works for a while, but the cumulative weight of these one-off solutions eventually starts pulling the entire engineering team down.

The limits of ad hoc scripts and tribal knowledge

The first signs of trouble are inconsistencies. Team A’s services run on a slightly different base image than Team B’s because they were configured months apart. One developer’s local environment works perfectly, while a new hire spends their first week fighting with dependencies because a setup script is outdated. It is a direct tax on productivity.

These improvised and undocumented setups end up creating recurring problems. Staging environments start drifting away from production as manual changes accumulate. As a result, deploys become less predictable.

Engineers end up getting pulled into tasks that have no direct connection to the product, such as debugging infrastructure, manually provisioning resources, or investigating deployment failures caused by differences between environments.

Pipelines start depending on specific scripts run locally or CI configurations that are difficult to understand and maintain. And new engineers face a steeper learning curve, not only to understand the codebase, but also to figure out which tools and processes they need to use in order to work on the system.

When “you build it, you run it” hits a limit

The principle of “you build it, you run it” is a great way to create a sense of ownership. A product team is responsible for its service, from code all the way to production. In a small company, this works well. At scale, the problems start to show.

When you have five, ten, or twenty teams all following this principle on their own, duplication appears. Each team builds its own Terraform modules for the same S3 bucket configuration. Each team creates its own alerts for CPU usage. Each team writes its own deploy pipeline for a standard web service.

This model creates bigger problems. The company pays for the same infrastructure and CI/CD work repeatedly, solved in slightly different ways by each team. Without a centralized approach, ensuring that all services follow security best practices or compliance standards becomes almost impossible. The burden ends up falling on individual teams that may not have the required expertise. Product engineers are forced to become specialists in Kubernetes, cloud networking, and observability tools just to keep their features running. Their attention gets split between building the product and managing its operational details.

Platform engineering as an evolution of DevOps culture

The answer is to evolve the environment where developers work while keeping the principle of ownership. That is the idea behind platform engineering. It changes the question from “How do we help each team run their own infrastructure?” to “How do we provide a platform that makes running infrastructure simple and consistent for everyone?”

Going beyond tool-centric DevOps

Many organizations see DevOps only as a set of tools such as CI/CD servers, infrastructure-as-code files, and monitoring dashboards. A platform approach is more about offering shared services and clear workflows.

Instead of simply giving developers raw access to cloud provider tools, a platform offers a higher level of abstraction. A developer should not need to write a complex CI/CD pipeline from scratch. They should be able to add a simple configuration file in the repository that connects to a standard pipeline managed centrally.

This means treating your internal infrastructure as a product. Your developers are your users. The success of the platform is measured by how much faster and more reliably they can deliver value to real customers. Self-service is a huge part of this. A developer should be able to create a new testing environment or check service logs without opening a ticket and waiting for another team.

Redefining roles and responsibilities in a scaling DevOps culture

This shift changes team structure and responsibilities. A common and effective pattern is to create a dedicated platform engineering team. This team is different from a traditional operations team that only acts as a gatekeeper. Its main job is to improve the developer experience and make engineers more effective.

The platform team concentrates the operational knowledge of the system. They build and maintain the core infrastructure, the CI/CD systems, and the observability stack. They also provide the tools and services that product teams use every day.

This creates a collaborative relationship. The platform team builds the standard path for development, and product teams use that standard to move faster. Product teams remain responsible for their own services, but the heavy lifting of the underlying infrastructure is already solved for them. That way they can focus on business logic, while the platform team ensures the infrastructure is secure and reliable.

Building your internal developer platform

Creating a platform is an ongoing process of identifying what slows developers down and building solutions to fix it.

Defining platform capabilities

Start by identifying the most common needs and problems across your engineering teams. The first platform services usually cover a few core areas.

You can offer standardized CI/CD pipelines, with reusable templates or workflows that handle build, testing, security analysis, and deployment for common application types. This way the developer only needs to define what is specific to their service.

Another important area is environment management, offering a simple way to create and destroy development, testing, and staging environments that stay consistent with production.

It is also worth centralizing logs, metrics, and traces. Services can be instrumented automatically with standard configurations, giving teams immediate visibility without requiring a lot of manual setup.

Finally, provide a secure way to manage application secrets and adopt strong security practices by default, such as secure network rules and IAM policies.

Adopting a product mindset for internal tools

Your platform will only succeed if developers actually use it. You cannot simply build it and assume they will show up. You need to treat it like any other product.

That means collecting feedback constantly. Talk to developers, run quick surveys, and create open office hours to answer questions and understand how they work and where they struggle. Use that information to decide what to build next.

Prioritize initiatives that solve real and recurring problems for multiple teams, not just ideas that seem technically interesting. Whenever possible, try to quantify the impact. Ask whether a new tool will save each developer an hour per week or reduce deployment failures by 50%.

It is also important to measure success by tracking adoption of platform services and developer feedback. Look at metrics such as deployment frequency, lead time for changes, and mean time to recovery. A good platform should improve these numbers.

Principles for a platform team

The culture of a platform team determines whether it becomes a help or an obstacle. Some principles tend to work well for successful platform teams.

Focus on helping developers, not controlling them. The platform should provide a standard path, with strong tools and well-supported patterns, but it should also allow alternatives when a team has a good reason to do something differently.

Treat platform services as internal products with clear APIs, good documentation, and easy-to-find materials. Build something small that already delivers value, put it into use, and evolve it based on feedback from teams. The idea is to avoid long cycles trying to build a “perfect” solution that may not solve what developers actually need.

It is also important to communicate continuously. Announce new features, changes, and deprecations through internal demos, posts, or newsletters. Teams should always know what the platform offers today and what is changing.

Making the transition without causing disruption

The goal is to evolve infrastructure gradually without needing to stop product development for a year to rebuild it.

Start with the biggest problem, the thing that generates the most complaints or wastes the most time. It might be inconsistent local environments or the manual process for creating a new service. Solve that first.

For each new platform service you build, provide clear instructions and support to help teams migrate from the old improvised way of working. Sometimes that means creating tools that automate parts of the migration.

The first team that adopts your new CI/CD pipeline and cuts deploy time in half will likely become a strong advocate for the change. Results like that help build momentum and encourage other teams to adopt the new approach.

Game is done…kinda”

Where we are?

https://depoco.itch.io/holy-carp

Ok, so the game is mostly done. I’ve uploaded it to itch.io. It’s still missing a few elements, but I’ll be adding more as I go.

This is only my second game ever, and honestly, I’m pretty happy with how it turned out. Of course, I wanted it to have tons of features and amazing gameplay, but those will come, step by step.

In this post, I’ll dive deeper into my GameManager and some refactoring I did, the good stuff that keeps the project tidy and manageable.

What needs to be added

Some of the pixel sprites needs to be fixed, it seems that they got messed up when I imported it into Godot, no idea why.

Sort out the menu and make it look prettier and at the same time add the audio I have + the settings window.

GameManager stuff

So this is my final script for the GameMananger:

extends Node

signal lives_changed(new_lives)
signal score_changed(new_score)


var lives = 3
var current_score = 0

# KEEPING SCORE ---------------------------------

func _update_score(fish_score):
  current_score += fish_score
  score_changed.emit(current_score)

#------------------------------------------------

# HEALTH ----------------------------------------

func _decrease_life():
  if lives > 0:
    lives -=1
    lives_changed.emit(lives)

  if lives == 0:
    _game_over()

#------------------------------------------------

# GAME UI STUFF ---------------------------------

func _game_over():
  var game_over_scene = preload("uid://behejrhw6wlo7")

  var game_over_instance = game_over_scene.instantiate()
  var GameOverCanvas = get_node("/root/MainScene/GameOverCanvas")

  GameOverCanvas.add_child(game_over_instance)

  get_tree().paused = true

#------------------------------------------------

func reset_game():
  lives = 3
  current_score = 0
  lives_changed.emit(lives)
  score_changed.emit(current_score)

It keeps track of score, lives and game over.

This is just a quick update. I’m starting a new game and have a lot on my plate right now, so progress on this post will be occasional.

I’ll be adding more to it from time to time. Also, keep an eye out for new posts about my new game. I plan to release Part 1 next week.

My dear readers. Remember that everything I write is not always 100% correct. I do mistakes and I’m a novice at this. So if you find a mistake or just have some random feedback on anything. Please comment!

The Engineering Manager as Coach, Not Boss

I want you to think about the best manager you have ever had. Not the most technically impressive one, not the one who shipped the most features, but the one who made you genuinely better at your job. I know who that person is for me, and I bet you know who that person is for you too.

Odds are good that person did not micromanage your work. They probably did not hover over your PRs or tell you exactly how to solve every problem you brought to them. What they likely did was ask you questions that forced you to think more clearly. They gave you feedback that was specific enough to actually act on. They took your career seriously in a way that felt genuine rather than performative. They helped you see the gap between where you were and where you were capable of getting, and then they helped you close it.

That is coaching. And it is the most underused and underdeveloped skill in engineering management.

Most engineering managers are never taught how to coach. They come up through the IC track where performance is individual, then they get promoted and suddenly they are responsible for other people’s growth without any real framework for how to do it. The instinct that fills the gap is usually managing – telling people what to do, reviewing their work, tracking their progress – rather than coaching – helping people develop the capacity to figure out what to do themselves.

That distinction matters more than it might sound. A team of engineers who are managed well will execute against a clear plan competently. A team of engineers who are coached well will grow, take on more complex work, make better decisions independently, and stay longer because they feel like they are actually developing as professionals. The output difference between those two teams compounds significantly over time.

So let’s get into what coaching actually looks like in the day-to-day context of engineering management.

The one-on-one is your primary coaching surface. Not a status meeting. Not a task review. The one-on-one is where you develop your understanding of what this person is working on, what is getting in their way, what they are learning, and what they are ready for next. Done well, it is where the most important growth conversations happen.

Most one-on-ones fail because they do not have a clear purpose and the manager ends up driving the conversation around whatever is top of mind for them – team updates, upcoming deadlines, process concerns. Flip that. The one-on-one belongs to the engineer. Open with something like: “what is on your mind this week?” or “what do you most want to talk through today?” Then actually follow their lead.

What you are listening for is not just the content but the subtext. An engineer who says “this sprint has been a bit slow” might be telling you they are bored. An engineer who says “I have just been heads down in the ticket queue” might be telling you they feel disconnected from meaningful work. An engineer who mentions a difficult interaction with a product manager three times across different conversations is probably telling you something important without saying it directly. Coaching requires learning to hear those signals and respond to them honestly rather than staying on the surface.

Questions are your most important tool as a coach. The instinct for most technically strong managers is to give answers. Someone comes with a problem and you tell them how to solve it. That feels productive and it often is in the short term. But it creates dependency. Every time you give someone an answer you had the chance to help them develop the capability to generate that answer themselves, and you declined it.

The shift is to respond to problems with questions. Not in a frustrating Socratic way where you refuse to ever be direct, but in a way that draws out the engineer’s own reasoning before you add yours. “What have you already tried?” “What do you think is causing it?” “If you had to guess at the best path forward, what would you say?” These questions accomplish two things simultaneously: they give you information about how the engineer is thinking, and they often help the engineer arrive at the answer themselves, which is a much more durable outcome than being told the answer.

When you do share your perspective, frame it as a perspective rather than a directive. “Here is how I would think about this” rather than “here is what you should do.” That keeps the ownership with the engineer and signals that their judgment is part of the equation, not just an obstacle between them and the right answer.

Performance conversations are where a lot of engineering managers lose their nerve, and that avoidance does real damage. When someone’s performance is not where it needs to be, the kindest thing you can do is tell them clearly and specifically, with enough context that they can actually act on it. Delayed feedback is not compassion. It is conflict avoidance dressed up as patience, and it ends up being far harder on the engineer in the long run.

The structure that works for performance conversations, whether they are corrective or developmental, follows a consistent pattern. Specific observation first – not “your code quality has been slipping” but “the last three PRs you submitted had significant issues that were caught in review and would have caused problems if they had gone to production.” Then impact – why this matters for the team, the product, or the engineer’s own growth. Then genuine curiosity – what is going on from their side, what might be contributing to this, what do they think they need. And then a clear shared understanding of what improvement looks like and a timeline for revisiting it.

The specific observation part is what most managers skip or soften into meaninglessness. “I just want to flag that some people have mentioned concerns about collaboration” is not actionable feedback. “In the incident last week, two engineers told me they felt their concerns were dismissed when they raised them in the thread – I want to understand what happened and work through it with you” is actionable feedback. Specificity is the thing that makes feedback usable rather than just uncomfortable.

Growth plans are one of the most underutilised management tools I know. Not the formal HR document kind that gets filled out once a year and then filed – the real kind, where you and an engineer have an honest conversation about where they want to go, what is in the way, and what you are both going to do about it.

A useful growth plan starts with a genuine conversation about what the engineer actually wants. Not what you think they should want, not what the standard career ladder says they should be aiming for. What do they actually want? More technical depth? Leadership experience? Exposure to a different part of the stack? The answer to that question changes what kinds of opportunities are meaningful to them and what kinds of stretch assignments will actually develop them rather than just adding to their workload.
From there, you build backward. If someone wants to move toward a staff-level role, what does the gap between where they are now and what that role requires actually look like? What specific skills, what visibility, what demonstrated impact would they need to show? Get specific enough that both of you could look at a piece of work and agree on whether it counts as progress toward the goal.
Then identify the opportunities. Not abstract goals – specific opportunities in the actual work ahead. This project coming up would give them the technical leadership visibility they need. This initiative has a coordination component that would develop their cross-team communication. This piece of the codebase is complex enough that owning it would give them the depth that is currently missing from their profile. When you can connect growth goals to real work, growth planning stops being a separate activity and becomes part of how you think about the team’s work anyway.
Revisit the plan regularly. Not in the annual performance review – in one-on-ones, every few weeks. “How is the work on that service going – is it giving you what you were hoping for?” That regularity signals that you are actually invested in the plan rather than using it as a box-checking exercise.

                      Career development conversations are different from growth plans in a specific way: they require you to engage with the engineer's ambitions beyond their current role, and sometimes beyond your team. This is uncomfortable for some managers because it feels like you are helping someone leave. In reality, you are building the kind of trust and loyalty that makes people want to stay.

The engineers who leave most readily are the ones who feel invisible – whose manager has never asked where they want to go, who cannot see a path forward from where they are. The engineers who stay in companies that would otherwise lose them are often the ones who have a manager who takes their development seriously enough to have honest conversations about what they want, even when the answer is complicated.

If an engineer tells you they want to move into management, help them explore that seriously rather than steering them back toward IC work because you need their technical contribution. If someone tells you they want to work on a different kind of problem than your team addresses, help them think through what that could look like internally before they start looking externally. That kind of engagement builds a relationship where the engineer feels like their interests are actually considered, not just tolerated.

Feedback culture within a team starts with how the manager gives feedback, but it does not stop there. If you want engineers to give each other honest, constructive feedback – in code review, in design discussions, in retrospectives – you need to model it and you need to make it safe. That means appreciating directness when you see it even when it is directed at you, and explicitly naming feedback behaviours you want to see more of when they show up.

One habit that is underrated: giving positive feedback with the same specificity you bring to corrective feedback. “Good job on that feature” lands with almost no impact because it tells the engineer nothing about what specifically was good. “The way you handled the rollback plan on that deploy was exactly the kind of thing I want to see more of – you anticipated the failure mode before it happened rather than waiting for it” – that lands. It tells the engineer something specific about their performance and it communicates something about your values. Both of those things are useful.

The gap between a manager who manages and a manager who coaches is ultimately a gap in how they think about their job. A manager who manages thinks their job is to get work done through their team. A manager who coaches thinks their job is to develop people who can get increasingly complex and important work done without needing to rely on the manager as a decision-making crutch.

The first version of that job has a ceiling. The second version compounds.
Engineers who work for coaches become more capable over time. They take on bigger problems, mentor more junior colleagues, contribute to the team in ways that go beyond their individual output. They are also, in my experience, far more likely to speak honestly with you about what is working and what is not – because the coaching relationship is built on genuine engagement rather than authority, and that changes what people are willing to say.
That honesty, in the end, is one of the more valuable things you can build. Because you can only fix the problems you know about.

Next in the series: Technical Debt – When to Fix, When to Ship. A framework for making trade-off decisions and communicating them to stakeholders who care about outcomes, not architecture.

How to Find a User’s Rank Based on Points Using Django ORM

If you’re building something like a leaderboard, a fitness tracker, or any competitive feature, ranking users by their score is something you’ll run into pretty quickly. It sounds simple — “just sort them and pick the position” — but when you get into Django ORM, there are actually a few different ways to do this, and they’re not all equal. Let me walk you through all of them, explain how each one works, and tell you which one I’d actually use in production.

The Setup

Let’s say you have a User model with an id, name, and points field. Something like this:

id name points
1 Alice 150
2 Bob 200
3 Carol 100

Bob has the highest points, so he should be ranked 1st. Alice is 2nd, Carol is 3rd. Simple enough in concept — now let’s look at the different ways to get there in code.

Approach 1: Window Functions (The Proper Way)

Django has had support for window functions since version 2.0, and if you’re not using them, you’re missing out. This is the cleanest, most scalable solution.

from django.db.models import F, Window
from django.db.models.functions import Rank

users_with_rank = User.objects.annotate(
    rank=Window(
        expression=Rank(),
        order_by=F('points').desc()
    )
)

# Get Bob's rank
bob_rank = users_with_rank.get(id=2).rank
print(f"Bob's Rank: {bob_rank}")  # Bob's Rank: 1

This runs a single SQL query and calculates the rank for every user in one shot. The Window class tells Django to use a SQL window function, Rank() handles the ranking logic, and order_by=F('points').desc() makes sure the person with the most points gets rank 1.

One thing to know about Rank(): it handles ties the same way SQL’s RANK() function does — if two users have the same points, they get the same rank, and the next rank is skipped. So if Alice and Bob both had 200 points, they’d both be rank 1, and Carol would jump to rank 3 (no rank 2).

If you don’t want that gap, use DenseRank() instead:

from django.db.models.functions import DenseRank

users_with_rank = User.objects.annotate(
    rank=Window(
        expression=DenseRank(),
        order_by=F('points').desc()
    )
)

With DenseRank(), tied users both get rank 1, and the next user gets rank 2 — no skipped numbers.

Approach 2: Subquery / Count Filter (Quick and Simple)

This one is easy to understand and works without any special imports. You count how many users have more points than the user you’re looking at, then add 1.

specific_user = User.objects.get(id=2)  # Bob
user_rank = User.objects.filter(points__gt=specific_user.points).count() + 1
print(f"Bob's Rank: {user_rank}")  # Bob's Rank: 1

This makes two queries — one to fetch Bob, one to count users above him. For a small table, that’s totally fine. The problem is it doesn’t scale well. If you need ranks for 50 users, you’d run 51 queries. And it doesn’t gracefully handle ties — if two users are tied, they’ll both show different ranks depending on who you ask about first.

Use this approach when you need the rank of a single user on a small dataset and you want to keep the code dead simple.

Approach 3: Raw SQL

Sometimes you just want to drop into raw SQL and be done with it. Django lets you do that with raw():

users = User.objects.raw("""
    SELECT id, name, points,
           RANK() OVER (ORDER BY points DESC) AS rank
    FROM myapp_user
""")

for user in users:
    print(f"{user.name}: Rank {user.rank}")

This works, and it gives you full SQL flexibility. But you lose all the ORM benefits — no composability with other querysets, no easy filtering afterward, and you have to manually handle the table name. I’d only reach for this if the ORM genuinely can’t express what you need, which is rare.

Approach 4: Python-Side Sorting

This one’s not a database approach at all — you pull everyone into memory and sort in Python:

all_users = list(User.objects.order_by('-points'))
for index, user in enumerate(all_users, start=1):
    user.computed_rank = index

# Find Bob
bob = next(u for u in all_users if u.id == 2)
print(f"Bob's Rank: {bob.computed_rank}")  # Bob's Rank: 1

This is fine if you’re already loading all users for other reasons (say, rendering a full leaderboard page). But if you’re just doing this to find one person’s rank, pulling the entire table into Python memory is wasteful. It also doesn’t handle ties — everyone gets a unique rank even if scores are equal.

Approach 5: Caching the Rank (For High-Traffic Apps)

If your leaderboard gets hit thousands of times a minute and the scores don’t change that often, computing the rank on every request is unnecessary load on your database. A common pattern is to compute ranks periodically and cache them:

from django.core.cache import cache
from django.db.models import F, Window
from django.db.models.functions import Rank

def get_cached_rankings():
    cached = cache.get('user_rankings')
    if cached:
        return cached

    users = User.objects.annotate(
        rank=Window(
            expression=Rank(),
            order_by=F('points').desc()
        )
    ).values('id', 'rank')

    rankings = {u['id']: u['rank'] for u in users}
    cache.set('user_rankings', rankings, timeout=300)  # Cache for 5 minutes
    return rankings

def get_user_rank(user_id):
    rankings = get_cached_rankings()
    return rankings.get(user_id)

This way, the expensive window function query only runs once every 5 minutes (or whatever interval makes sense for your app). Every other request just reads from cache. You can trigger a cache invalidation whenever a user’s score changes if you need it to be more real-time.

Which Approach Should You Use?

Here’s a quick breakdown:

Approach Best For Watch Out For
Window Functions Most cases — clean, scalable, handles ties properly Requires Django 2.0+ (not an issue in practice today)
Count Filter Single user rank, small dataset, simple code Multiple queries, doesn’t handle ties well
Raw SQL Complex ranking logic the ORM can’t express No ORM composability, manual table names
Python Sorting Already loading full list for other reasons Memory usage, doesn’t handle ties
Caching High-traffic apps with infrequent score changes Stale data between cache refreshes

Honestly, for 90% of use cases, window functions are the right answer. They run a single efficient query, the database handles all the logic, ties are handled correctly, and the code is readable. The only time I’d deviate is if I need real-time ranks on a very high-traffic endpoint — in that case, I’d combine window functions with caching.

The count filter approach is fine for a quick prototype or a low-traffic admin page. Raw SQL is a last resort. Python sorting is situational. And caching is a performance optimization you layer on top of whichever database approach you pick.

Wrapping Up

Ranking users in Django isn’t complicated once you know your options. Start with window functions — they’re what the database is built for and Django exposes them cleanly. If you run into performance issues at scale, add caching on top. And if you’re prototyping quickly, the count filter approach will get you there in two lines.

The main thing to decide early is how you want to handle ties, because that choice shapes which function you reach for. Get that right upfront and the rest is straightforward.

Strategy vs. Execution: How Leaders Set Technical Vision

There is a version of technical leadership that looks like this: you are deep in a sprint, your team is shipping, the product roadmap is clear, and everyone knows what they are building. Everything is humming. You feel productive. You feel useful.

And then six months later you look up and realise you have been executing against a direction that no longer makes sense. The architectural decisions made eighteen months ago are now actively fighting the product requirements coming in today. The platform you are running on does not support the scale you need. The technical debt you kept deferring is now the reason your velocity has halved.

This is what happens when execution runs ahead of strategy. And in my experience, it is one of the most common failure modes in technical leadership, because the short-term feedback loop for execution is so much tighter and more satisfying than the long-term feedback loop for strategy.

Execution tells you immediately whether it worked. Strategy takes months or years to validate, and by then the leader who made the call may not even be around to learn from it. That asymmetry creates a natural gravitational pull toward doing over thinking, shipping over planning, tactical clarity over strategic ambiguity. Understanding that pull is the first step toward resisting it when you need to.

This article is about the work that sits above execution: how senior engineering leaders set technical direction, how you align that direction with product and business goals, how you communicate it to the people building against it, and how you maintain it over time as circumstances change.

Setting technical vision starts with a question that sounds simple but is actually quite hard to answer well: what problem is this engineering organisation trying to solve? Not “ship features quickly” — that is a capability, not a purpose. Not “build a reliable platform” — that is a property, not a direction. The real answer connects technical work to business outcomes and to user needs in a specific and falsifiable way.

Something like: we are building a data infrastructure that lets our analysts answer any product question within four hours without engineering support. Or: we are rebuilding our checkout system to reduce payment failure rates below one percent so we can expand to markets where card infrastructure is unreliable. Or: we are investing in observability tooling so that the time between a production anomaly and a diagnosis is under fifteen minutes.

These are statements that have a clear definition of success attached to them. They tell engineers not just what to build but why, and they connect technical decisions to outcomes that the business actually cares about. When your technical vision has that kind of specificity, it becomes a useful decision-making tool. Engineers can look at a proposed piece of work and ask: does this move us toward the goal? If the answer is no, it does not necessarily mean the work is wrong, but it raises a question that is worth answering.

Without that specificity, technical vision tends to drift into vague aspiration. “We want a clean, scalable, well-tested codebase” is aspirational, but it does not tell anyone what to prioritise when they have to choose between three competing things. Vision that cannot guide a tradeoff is not really vision. It is decoration.

Aligning technical direction with product and business goals is where most of the actual work of technical leadership happens, and it is messier than the frameworks make it sound. Product and engineering are not always pulling in the same direction. Business stakeholders have timeframes and objectives that do not always map cleanly onto technical realities. There are genuine tensions that have to be surfaced and worked through, not papered over.

The most effective technical leaders I have observed spend a disproportionate amount of their time in conversations that are not with their engineering team. They are in product reviews. They are in quarterly planning sessions. They are talking to the finance team about what the infrastructure cost structure actually looks like. They are in customer calls listening to the friction points that end up becoming product requirements six months from now. They are building enough context about the business that their technical direction is grounded in something real rather than purely in engineering instinct.

This matters because technical decisions made in isolation from business context tend to be technically elegant and practically wrong. A beautiful microservices architecture might be the right long-term call, but if the business needs to move fast in the next twelve months and your team has four engineers, it might be the wrong short-term one. The technical leader who understands the business well enough to make that call explicitly, rather than defaulting to the architecturally “correct” answer, is significantly more valuable than one who does not.

Roadmap ownership is the operational expression of technical vision. A technical roadmap is not a feature list. It is a sequenced set of investments that tell a coherent story about how the system is evolving and why. The sequencing matters as much as the content, because it reflects your actual prioritisation under real constraints rather than an idealised list of everything you want to do.

When I think about what makes a good technical roadmap, a few properties matter. It is honest about current state. It does not pretend that the existing system is further along than it is, because that dishonesty creates misleading expectations. It distinguishes between investments that enable future capability and investments that address current risk. Those are different categories with different urgency profiles. It has explicit tradeoffs documented. When you chose to sequence thing A before thing B, there was a reason. Write it down so that when circumstances change and someone asks “can we move B up?”, you have the context to answer that question thoughtfully rather than from gut instinct.

And crucially, the roadmap is a living document, not a contract. One of the most common problems I see with technical roadmaps is that they get published once and then decay. The codebase evolves, the product requirements shift, new information comes in — and the roadmap that was accurate six months ago is now quietly misleading anyone who reads it. Treat your roadmap as something that needs regular maintenance, not something that needs a polished quarterly reveal.

Communicating technical direction is a distinct skill from having it, and it is one that engineers who move into senior leadership roles often underestimate. The way you talk about technical strategy to your engineering team is different from how you talk about it to your product counterparts, which is different again from how you talk about it to executive stakeholders.

To your engineering team, the goal is to give people enough context that they can make good local decisions without needing to escalate everything. You want them to understand the direction well enough that when they encounter an ambiguous situation, they can reason about what the right call is rather than waiting for guidance. That means going deep on the why: why is this the direction, what are the tradeoffs we considered, what would make us revisit this. Engineers who understand the reasoning behind a decision are better positioned to execute against it and to flag when something they are seeing in the work suggests the reasoning might be wrong.

To product and business stakeholders, the translation layer is about outcomes and risk, not architecture. “We are building a service boundary between the payments system and the billing system” is not a compelling framing for a product conversation. “We are making a structural change that will let us run payment experiments independently of billing changes, which should cut our experiment cycle time by about half” is a compelling framing, because it connects the technical work to something product actually cares about.

Getting good at this translation is not about dumbing things down. It is about understanding what the other person actually needs to know to make good decisions or to trust that you are making good decisions. That is a different question than “what do I know about this topic.”

One of the tensions that comes up consistently in technical leadership is the relationship between strategic investment and near-term delivery pressure. There is almost always more pressure to ship features than there is to invest in the platform, pay down debt, or make structural improvements that will pay off over time. That pressure is legitimate — the business has real needs — but if it never gets balanced against platform investment, you end up in a state where the platform is actively fighting your ability to deliver.

The framing that I have found most useful for navigating this tension is to treat platform investment not as something that competes with delivery but as something that enables delivery over a longer time horizon. The argument is not “we need to stop shipping features to fix the platform.” The argument is “here is what the platform investment unlocks, here is the timeline on which it pays off, and here is the cost we are currently paying by deferring it.” That framing, with specific numbers attached when possible, tends to land better with business stakeholders than an abstract argument about technical health.

It also requires you to do the work of actually knowing what the cost of the current state is. That means having data. How much engineering time per sprint is going to work that would not be necessary if the platform were in better shape? What is the approximate blast radius of the current debt in terms of velocity impact? How many incidents in the last quarter were attributable to the areas you are proposing to invest in? This is the kind of evidence that turns a technical argument into a business argument.

There is a version of technical leadership that is mostly reactive: responding to product requests, triaging incidents, making tactical calls as they come up. That version can look like strong leadership from the outside because things are moving. But it does not create the conditions for the team to do its best work over time. The conditions for sustained, high-quality engineering output require a clear technical direction, explicit prioritisation, and the kind of structural investment that only happens when someone with enough context and enough authority is thinking more than one quarter ahead.

That is what technical vision is actually for. Not the vision document, not the architecture diagram, not the quarterly roadmap review. The underlying habit of mind that keeps asking: given everything we know about where this product and this business are going, are we building the system that will get us there? And if the honest answer is no, being willing to say so and to do something about it.

That willingness is what separates technical leaders from technical managers. And it is more about courage than it is about technical knowledge.

Next in the series: The Engineering Manager as Coach — practical techniques for performance conversations, growth planning, and career development.

Building Psychological Safety in Engineering

Let me ask you something uncomfortable: when was the last time someone on your team told you they had made a mistake before you found out about it yourself?

If you have to think hard about that, or if the honest answer is “I am not sure that has ever happened,” you probably have a psychological safety problem. Not a people problem. Not a talent problem. A culture problem, and one that sits squarely in your lap as the person responsible for setting the conditions under which your team operates.

Psychological safety is one of those concepts that gets talked about in engineering leadership circles to the point where it starts to feel like a buzzword. But the research behind it is solid, and the practical reality is straightforward: engineers on teams where they feel safe to take risks, speak up, and make mistakes without fear of punishment consistently outperform engineers on teams where they do not. Google’s Project Aristotle found it was the single biggest predictor of team effectiveness across hundreds of internal teams. It matters more than individual talent, more than technical skill, more than compensation.

So why is it so rare? Partly because it is genuinely hard to build. Partly because the behaviours that build it are counterintuitive for technically minded people who have been rewarded their whole careers for being right. And partly because leaders often think they have it when they do not, because the absence of psychological safety is largely invisible from the top.

Here is what I mean by that. When engineers do not feel safe, they do not tell you. They work around problems quietly. They do not raise concerns in meetings. They stay silent when they disagree. They fix bugs without mentioning them. They leave rather than confront a difficult situation. From your vantage point as a manager, everything might look fine. The problems are just invisible, which is exactly what makes the culture so hard to improve from the inside if you are not actively looking for the signals.

So let’s talk about what to look for, what to build, and what to stop doing.

The first signal to watch is who speaks in group settings. In a psychologically safe team, contributions are distributed. Junior engineers raise concerns. Senior engineers acknowledge uncertainty. Different people push back on ideas in different meetings. If you notice that the same two or three people do all the talking, or that nobody ever disagrees with whoever has the most tenure, or that critical questions always come up in private after the meeting rather than in the meeting itself, those are meaningful signals. The information is moving, but it is moving through channels that feel safer, which means you are not actually getting it when and where it matters.

The second signal is how the team handles incidents and failures. This is the clearest window into your culture. When something breaks in production, what happens? Is the post-mortem focused on understanding the failure and improving the system, or does it subtly or not so subtly focus on who was responsible? Do engineers run toward an incident or away from it? Do people escalate early when something is going wrong, or do they hold out hoping to fix it themselves rather than surface the problem?

I have seen teams where engineers would rather work through the night on a production issue than escalate because they were afraid of how the escalation would be received. That is not dedication. That is fear. And it is a direct result of an environment where being the person who raised the alarm felt more dangerous than trying to quietly fix the problem.

The blameless post-mortem is the standard recommendation for addressing this, and it is a good one – but only if it is actually blameless. A lot of post-mortems that are nominally blameless still subtly assign fault through the framing of their questions: “why did the engineer merge without a code review” rather than “what in our process allowed this change to go out without sufficient review.” The difference in those two questions is the difference between a culture that learns and a culture that punishes while pretending not to.

Write your post-mortem templates with this framing explicitly built in. Questions like: what in our system made this failure possible, what would have needed to be different for this to have been caught earlier, what can we change about our process to prevent this class of failure. Not: who approved this, why was this not caught, whose responsibility was this. If you run post-mortems this way consistently, over time it changes how the team thinks about failure – as information about the system, not as evidence of someone’s incompetence.

Feedback loops are the second major lever. A team without healthy feedback loops is a team where problems silently compound until they become crises. Building those loops requires two distinct things: creating channels for feedback to flow, and demonstrating through your own behaviour that feedback is welcome and acted on.

The first part is structural. Regular one-on-ones where you ask specific questions rather than open-ended check-ins. Retrospectives that have genuine psychological safety built into them (anonymous input tools can help here if the team is not yet comfortable with open discussion). Skip-level conversations if your org is large enough. Anonymous pulse surveys for tracking sentiment over time. These are all mechanisms for surfacing information that would otherwise stay invisible.

The second part is behavioural and it is more important. If engineers give you feedback and nothing happens, they stop giving feedback. If you react defensively when someone raises a concern, they stop raising concerns. If you say you want honesty but subtly reward people who tell you what you want to hear, you will get people who tell you what you want to hear.

The most powerful thing you can do to build feedback culture is to model receiving feedback well. When someone tells you something uncomfortable, thank them specifically for the specificity of the feedback. Then act on it and tell them you acted on it. Do this consistently and over time it signals that feedback in this team is not just tolerated but genuinely valued and used. That signal compounds. People talk to each other about how you respond, and your reputation as someone who receives feedback well becomes one of the structural features of your team’s culture.

Ask for feedback on yourself directly and explicitly. Not “any feedback for me?” in a one-on-one where the power dynamic makes honest negative feedback almost impossible. More specific prompts: “I ran that planning session last week – was there anything about the format that was not working for you?” or “I made a call on the architecture last month and I have been wondering whether I handled that well – what was your read on it?” Specific questions lower the barrier enough that more honest answers become possible.

The third lever is how you respond to mistakes, and this is the most visible signal your team gets about what the culture actually is regardless of what you say it is.

When an engineer makes a significant mistake – pushes a bug to production, misestimates badly, handles a difficult customer situation poorly – your response in that moment is teaching the whole team what the rules are. Not just the person involved. Everyone who hears about it is watching what happens.

The response that builds psychological safety has a few consistent properties. It is proportionate to the situation. It focuses on understanding what happened and learning from it rather than on assigning fault. It is private when the situation calls for privacy. It treats the person as an intelligent adult who does not need to be punished but who does need support in improving.

None of that means being soft on genuinely unacceptable behaviour. If someone repeatedly ignores code review conventions, ships without testing, or behaves disrespectfully to a colleague, those are different situations that call for direct and specific feedback. The distinction is between mistakes (things that happen despite good intentions and reasonable effort) and patterns of behaviour that reflect poor judgment or disrespect. Psychological safety is not about protecting people from the consequences of repeated poor behaviour. It is about ensuring that honest effort and reasonable risk-taking are not punished.

There is a specific failure mode worth naming here because it is common and because managers who fall into it often do not know they are doing it. It is what I think of as the chilling effect of public criticism. When a leader criticises an engineer’s work in front of the team (in a code review, in a meeting, in a Slack thread where multiple people are watching) the impact is not contained to that one person. Every engineer on the team who sees it learns something about the risk of being visible and vulnerable. The criticised engineer may recover. But the observation travels further than you think, and it quietly teaches people to keep their heads down.

Code review is where this plays out most often in engineering teams. Reviews where every comment is framed as a problem to be fixed, where reviewers never acknowledge what is working well, and where tone is dismissive or impatient — that culture compounds over time and eventually starts to affect how openly engineers communicate and how willing they are to take on work that is outside their comfort zone.

It is worth reviewing the norms around code review explicitly with your team. Not to make code review soft, but to make it genuinely useful. The goal of code review is to improve the code and grow the engineer. Both parts matter. A review that consistently improves the code but leaves the engineer feeling beaten up is a review that is failing at half its job.

One last thing that does not get enough attention: the connection between psychological safety and retention. Engineers leave teams for a lot of reasons, but one of the more consistent ones is the slow erosion of feeling like they can do their best work. That erosion is usually not a single incident but an accumulation of small signals – a feedback that was not heard, a mistake that was handled poorly, a concern that was dismissed, a pattern of who gets credit and who does not. It is invisible until someone puts in their notice, at which point the manager usually asks “what happened?” without realising the honest answer is “a lot of things, over a long time.”

Psychological safety is not a team-building exercise. It is not a workshop or a values statement. It is the cumulative result of thousands of small interactions

  • how you respond to a mistake
  • how you run a post-mortem
  • how you receive feedback
  • how you handle a tense code review

That either add up to a culture where people feel safe enough to do their best work, or they do not.

You build it slowly and you can damage it quickly. That asymmetry is worth keeping in mind every time you have one of those small interactions. Because you are always, whether you realise it or not, signalling to your team what the rules are.

Next in the series: Strategy vs. Execution — how senior engineering leaders align product, technology, and business goals into a coherent technical direction.