Introduction
My AI Pair Programmer was finishing up on a task I had given it, summarizing what it had done. I ran the app, and the feature I had asked for a few minutes back had been implemented perfectly.
This is not an unusual experience. I suspect this is what loads of programmers experience everyday. But results depend a lot on the complexity and uniqueness of the problem they are addressing, the language or framework they are using, the LLM that they employ, whether they are using code completion or a coding agent, the details they put into their prompts, and many other factors. There are enough variables to make AI Pair Programming a bit of an art.
The promise of telling AI what I want and having it spit out an app seems a few month away yet. In the interim, I had apps to build. There are some things I've learned and thought I'd share them with you.
To give you some context, I'm a fairly experienced developer. I've seen the sausage being made, and therefore don't trust the output of “no-touch” platforms like Replit, Lovable, Bolt or their ilk. This article is about IDEs that have either AI code completion or AI coding agents — Visual Studio Code with GitHub Copilot or Cline, and Windsurf.
Problems of coding agents
The following are the usual problems with coding agents:
Loss of train of thought: I've switched on the “auto-approve reading files” setting for my coding agents, so there really is no opportunity for me to guide them once I hit the “go” button. Often, it isn’t until I see the agent chasing its own tail, trying incessantly to solve a problem it has no idea about, that I click the stop button, revert all the changes it’s made in all the files, and try again. There’s no point in going ahead with this because in conversing with itself, trying to come up with a solution, the LLM will forget its original goal.
Unfamiliar with new developments: AI Pair Programmers trip up on material they haven't encountered. Since they don't search the internet when completing our code, new frameworks, packages or languages boggle them — Claude Code got bogged down by Tailwind version 4, for instance.
Tremendous power: One of the joys of AI Pair Programming is having a lot of code written for you. But, that's a double-edged sword: it has no compunctions about ripping up code which we think is unnecessary to touch. The entire codebase is only a draft in its view, and the strategy of building something up block by block is not something it understands. (Is this a failure of AI Pair Programmers? If we just want it to assist us in doing things the way we've always done, it might be. However, AlphaGo inventing a whole new way of playing Go comes to mind, and one is led to wonder if this is a whole new way of programming. If we have the power to take in the entire codebase all at once, are changes to any part of it in service of a goal, out of question?)
Non-deterministic: The code that your AI Pair Programmer writes one time might be different from what it writes the next time. Naming conventions, elegance, readability, ways of abstracting, can be inconsistent throughout the code. (Again, will this variability be something we have to learn to be at peace with or will a convention arise within these AI Pair Programmers in the future? If the AI Pair Programmers are eventually going to be trained on AI generated code, an averaging can be expected eventually, and it might centre on some particular ways. Will we still be reading code then or moved to even more abstracted “programming” by then? All this remains to be seen.)
Mitigation strategies
Use a framework with guardrails: The web based application world I inhabit has a plethora of options for every part of the development process:
Language: Javascript, Typescript and others
Frameworks: React, Vue, and others
Meta-frameworks: NextJS, React Router, Solid, and others
State management: Redux, Zustand, Jotai and others
ORMs: Prisma, Drizzle, and others
Schema validation: Zod and others
Database providers: Supabase, Firebase, MongoDB and others
Hosts: Netlify, Vercel, Fly.io, Heroku and others
And, each of these offer you several ways of being used. So, a combination of these can be used in a hair-tearingly large number of ways. With non-deterministic AI Pair Programming thrown into the mix, you have a staggering amount of complexity to handle.
Use version control: Since codebases can be made unusable in a matter of minutes, a way to reverse an AI Pair Programmers enthusiasm is invaluable. Use branches and commit working code.
Picking Elm
If the amount of training data available was the only metric to go by, you would end up picking NextJS since there are just so many projects that use it. NextJS achieves its popularity by not having any opinions—any way that you choose to work is welcome. Take fetching data, for example. Depending on whether you are using a server component or a client, component, you have the choice of using fetch , an ORM, a hook, or a community library like SWR or React Query or several others. There are multiple ways to get data from the server and into your frontend.
I built a project with NextJS. While I could get a professional looking app built quickly using ShadCN, React's inability to provide a sane method of fetching data when a component loads left me wondering about my life choices. I switched to React Router since it offers up a loader and better defaults, but, again, it doesn't stop you from doing any old thing you please.
Then, I read Alex Russel's forceful argument for not defaulting to React. He urges us to use user experience as the criteria for choosing a framework.
I chose Elm. Yes, Elm. Yes, the last update to the language happened six years ago. However, it's a brilliant language and framework, and very usable. I think it is particularly well suited to AI Pair Programming for these reasons:
Elm has guard rails: The Elm Architecture is the only way that Elm apps can be written in. Types allow you to be expressive and the language enforces their use. There is no null type. A data model is the core of an Elm app and the flow of data is defined and predictable. Data exchange with the Javascript world is regulated and points of failure are known. If an Elm app compiles, you'll never have a runtime error. These sorts of guard rails help when using an AI Pair Programmer.
Error messages are legendary: Elm took the idea of human-readable error messages seriously. In Elm, errors are genuinely helpful. Suppose you had code like this:
type Animal = Cat | Dog | Llama
sound animal =
case animal of
Cat -> "Meow"
Dog -> "Woof"Elm will then give you an error like this:
MISSING PATTERNS
This `case` does not have branches for all possibilities:
5| case animal of
6| Cat -> "Meow"
7| Dog -> "Woof"
Missing possibilities include:
Llama
Add a branch to cover this pattern!While being helpful to humans, it is very useful to AI Pair Programmers as well.
Refactoring is easy: The sort of safety that Elm's guardrails gives you makes it incredibly easy for us to refactor code. Want to extract a bunch of code into a module so that our AI Pair Programmer can grok things better? Go right ahead! All the places that are affected by the change are surfaced and can be handled. The implication of this, going back to Alex Russell's insight of using user experience as the criteria for choosing a framework, is that we can provide a great user experience because iteratively refining the experience or adding entirely new features is a breeze.
Code is readable: Reading the code that AI generates is necessary since we aren't always sure if what was generated is what we actually want. Further, the Elm Formatter also standardizes how this code looks. Together, this makes Elm code incredibly easy to read and understand.
While those are the big reasons why Elm is a great choice for AI Pair Programming, its strict architecture, state management, and data interchange methods, make it very appealing to me.
The big detraction to Elm is that it has a lot of boilerplate code. If you introduce a new input, for instance, you'll have to create a field in your data model, create a message, and handle this message in the update function. But, this is exactly where AI shines — it can write that code for you with ease.
There are still ways to optimize working on an Elm app with your AI Pair Programmer though:
Break the app up into multiple modules centred around concepts. This makes it easy for you and your pair programmer to think about the app. If applicable, ask your AI Pair Programmer to create a model, update and view function inside the module, and just wire things together in the main module.
When the AI is getting things wrong, don't be hesitant to get in and write the code line by line. Oftentimes, Windsurf just picks up on what you are doing, and you can just tab-tab-tab your way through it.
Since Elm is such a quaint language, I've had the AI write code that sounds like it should exist but doesn't actually. Though infrequent, watch out for this.
Commit your code when a feature is finished.
For things that you cannot do in Elm, create a custom component. It can keep things Elm-like.
Ports are a great way to communicate with the Javascript world. Just make sure you aren't slowly pushing your app's logic to the Javascript side.
Conclusion
Elm is like a bento box: it has well defined areas that dictate where everything goes, while it wraps and protects from the outside world. This rock solid structure is what a framework is meant to be, and with Elm, an AI Pair Programming's tremendous power can be channeled.
