When teams pick a language for a new project, the criteria are familiar: hiring pool, ecosystem maturity, team familiarity, IDE support. When your primary developer is a coding agent, most of those criteria become irrelevant. The agent has no language preference. It needs no IDE plug-ins. Different factors take over: how well the model knows the language, how parseable the error messages are, how predictable the idioms are, how the runtime model shapes generated architecture.
I have been running agents on a Go project for the past month. Somewhere midway through I started wondering whether Go was the right call for a project where agents do most of the implementation. I do not have a universal answer, but I have a framework for thinking about the question.
What the model saw#
Code generation quality tracks training data representation. JavaScript and TypeScript dominate GitHub and Stack Overflow by volume. Every frontier model performs best on JS/TS generation tasks. Python is close behind but poorly suited for long-running services and daemon code. Go has decent but noticeably smaller representation. Rust, Elixir, and everything further out trail more sharply.
This is not abstract. My agents get the happy path right in Go consistently. The problems live in the corners. I watched agents produce goroutine leaks where a channel send had no receiver. Unbuffered channel deadlocks in timeout paths. Error wrapping that compiled fine but lost the sentinel value needed for errors.Is. A human Go developer catches these while writing because the patterns are muscle memory. The agent does not have muscle memory. It has statistical likelihood, and for Go’s concurrency primitives, that likelihood is lower than for a TypeScript async/await chain.
None of this made the output unusable. It meant more review iterations. A noticeable fraction of agent-generated PRs touching concurrency needed a fix for a subtle goroutine or channel issue before merging. The same agents writing straightforward HTTP handler code produced clean PRs on the first attempt almost every time.
The question each team needs to answer: does the added review cost for your chosen language outweigh the runtime benefits it provides? There is no universal answer. It depends on how much of your codebase involves concurrency and other advanced patterns versus straightforward request handling.
The feedback loop#
An agent does not read documentation before writing. It writes code, runs the toolchain, reads the output, and corrects itself. The quality of that correction cycle depends entirely on how structured the toolchain output is.
Think of it as a spectrum. At one end, TypeScript: run tsc --noEmit on a type mismatch and you get:
src/handler.ts(14,23): error TS2345: Argument of type 'string'
is not assignable to parameter of type 'number'.
File, line, column, error code, human-readable message. The agent reads that, locates the exact position, fixes the mismatch. One round-trip. ESLint catches bad patterns before the agent finishes writing a file. Prettier enforces formatting. The strict flag in tsconfig.json surfaces implicit any types that would otherwise slip through. Each tool is another correction signal the agent can act on without human involvement.
Go sits in the middle. go vet and staticcheck produce structured, machine-readable output. go test exits non-zero with clear test names and failure descriptions. But Go’s type system is simpler than TypeScript’s. Fewer type-level guarantees means fewer correction signals reaching the agent automatically.
At the other end, Python. A runtime TypeError surfaces only when execution reaches the offending line. MyPy helps, but many codebases do not enforce it consistently, so the agent cannot rely on static analysis being available. The primary error signal is a multi-line traceback the agent has to parse as prose.
The key insight: toolchain quality for agents is not the same as toolchain quality for humans. Humans read error messages contextually, inferring cause from stack traces and surrounding code. Agents need structured output in a predictable format. Languages where errors follow file:line:col: code: message give agents a direct correction loop. Languages where errors require interpreting contextual hints are harder to self-correct against. Place your language on that spectrum and you know how many review iterations to expect.
Verbose but predictable#
First instinct: Go is verbose, so it burns more context window per feature. More tokens in, more tokens out, higher cost, lower throughput. But look at what the verbosity actually consists of:
if err != nil {
return fmt.Errorf("opening config: %w", err)
}
This is one of the most predictable patterns in code generation. Agents reproduce it correctly every time. The pattern is so uniform across Go codebases that the model has seen millions of instances. There is exactly one way to propagate errors in Go, and every Go file demonstrates it.
The consistency extends beyond error handling. Go’s standard library conventions, interface patterns, and struct initialization follow narrow, well-established forms. An agent generating an HTTP handler in one package writes code structurally identical to the handler in another package. TypeScript’s flexibility (classes or functions, decorators or plain objects, named exports or default exports) means agent-generated code can drift in style across a project unless you constrain it heavily with linter rules.
TypeScript error handling specifically is a zoo. Open a single module in a mature codebase and you might find:
// Pattern 1: throw/catch
throw new AppError("not found", { status: 404 });
// Pattern 2: Result type
const result = parseConfig(raw);
if (result.isErr()) return result.error;
// Pattern 3: promise chain
fetch(url).then(handleOk).catch(handleErr);
// Pattern 4: async try/catch
try { await client.send(req); }
catch (e) { logger.error(e); }
Four patterns, one file, all valid. The agent picks whichever it saw most recently in the prompt context, which may not match your codebase’s convention at that call site.
Verbose but predictable beats compact but variable for agentic maintenance. This surprised me. The context window efficiency argument for terser languages is intuitive, but it falls apart when you measure consistency across hundreds of generated PRs. Predictability of patterns matters more than character count.
Concurrency by default#
For a daemon that manages subprocesses, detects timeouts, and monitors for stalls, the Node.js event loop is a fundamental constraint. A heavy JSON parse or a synchronous file read blocks stall detection for the entire process. In Go, each concern runs in its own goroutine:
go func() {
select {
case result := <-work:
handle(result)
case <-ctx.Done():
return
}
}()
An agent writing Go produces this shape naturally. Concurrency is the path of least resistance. An agent writing Node.js defaults to sequential code inside an async wrapper. You have to explicitly prompt for worker threads or cluster mode, and the agent sometimes gets the worker thread API wrong because fewer training examples exist for it.
I saw this play out repeatedly. Issues that required monitoring a subprocess while enforcing a timeout got concurrent solutions from the Go agent on the first attempt. Timeout in one goroutine, output processing in another, result collection via channel. The equivalent in Node.js would require deliberate reasoning about event loop blocking that the agent does not do unprompted.
A language’s defaults shape the agent’s default architecture. Go steers toward concurrent designs. Node.js steers toward sequential ones. Neither default is inherently wrong. But if your problem requires concurrency, a language where concurrency is the default path gives you better first-draft output from the agent.
This cuts both ways. If your application is request/response with no background processing, goroutines are unnecessary machinery. The event loop handles HTTP servers cleanly. Concurrency by default is an advantage only when your problem actually requires concurrency.
One binary, zero questions#
go build produces a statically linked binary. Copy it to the server. Run it. That is the entire deployment.
Node.js on a remote host means installing the runtime, running npm install or baking a Docker image, managing node_modules, and handling native dependencies if any package includes them. Every layer is another thing the agent can get wrong when generating deployment artifacts.
Agents write Dockerfiles well. But the Dockerfile itself is a cost: another artifact to maintain, another build step to debug, another moving part that can diverge from the development environment when a base image updates. Fewer moving parts means fewer failure modes in agent-generated infrastructure.
If your project deploys to a managed platform (Vercel, Cloudflare Workers, Lambda), this dimension carries no weight. The platform manages the runtime. For self-hosted services, daemons, and CLI tools, the single binary model eliminates an entire category of deployment failures that the agent would otherwise need to navigate correctly.
The gap is closing#
Sonnet 3.5 was writing decent Go in 2024. Claude 4 is noticeably better. The model generation cycle runs roughly six months. With each generation, training data grows and the quality gap between languages narrows. Building a strategic language choice on “the agent writes better TypeScript today” is building on a temporary advantage.
There is a counter-argument. The JavaScript ecosystem grows faster in absolute terms than any other language ecosystem. More npm packages published, more Stack Overflow answers posted, more GitHub repositories created every month. This produces a reinforcing loop through training data. The leading language keeps generating more training signal, so the gap might shrink slower than expected.
I do not know which effect dominates. Neither does anyone else. The practical implication: if you are choosing a language for a project that will run for three years or more, current generation quality is a weak signal. The model your agents use in year two does not exist yet. The runtime characteristics of your chosen language are fixed. Goroutines will still be goroutines. The event loop will still be the event loop.
If you need an answer today, TypeScript is a reasonable default. But defaults exist to be overridden when the problem demands it. Pick the language that fits the problem. The agent will adapt. It already does.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.