Most language choices are made for bad reasons. The last project used it, a conference talk was persuasive, the job description said so, or someone liked the syntax. A language outlasts the frameworks built with it, and often the manager who introduced it. A decision with that lifespan deserves a checklist.
What follows is that checklist, in the order I work through it. The criteria pull against each other, so the real work is deciding which costs fit the project.
1. Static type checks
A type checker is the most useful refactoring tool in a codebase. Rename a field or change a signature and the compiler lists every call site that needs attention, so a large codebase becomes something you navigate by following errors.
Types are also documentation that cannot go stale. A signature, a record, and a closed set of variants often describe enough of a module to use it without reading the implementation or trusting a comment that drifted two years ago.
Feedback arrives early as well. A mismatch caught in the editor costs seconds; the same mismatch in production costs an incident. An AI agent benefits twice: the compiler signals without running the system, and the types constrain what it may write next.
Enforcement matters more than syntax. Annotations the build does not check are commentary, and a strict system that developers routinely escape is a dynamic language with extra steps. Type design costs work, so a short-lived script may not repay it. For code several people maintain over months I treat static checking as a requirement.
2. Fast compile
The compiler sits inside the edit loop. When a cold build takes seconds I run it constantly. When it takes minutes I batch changes, switch tasks, and start guessing instead of checking.
Measure a cold build on a fresh CI runner, plus test startup and language server response, since a warmed incremental build hides the cost that new contributors and automation pay. Designs differ: some keep the model simple so builds stay fast, others spend the time on optimization and charge it to the edit loop.
A slow build hurts an agent more than a person. The agent tries something, reads the result, and corrects, so seconds per check let it take many small verified steps. With minutes per check it waits or guesses, and the result is a large change nobody validated.
3. Small binary or output
Deployment makes the output visible. A static executable, a managed runtime, an interpreter, and a bundled dependency directory are different artifacts with different operational work. A small executable does not always mean a small deployment, because the runtime and libraries count too.
Check the whole artifact: binary, runtime, assets, debug symbols, base image. An image that ships a runtime and a package directory can run to hundreds of megabytes before any application code exists. For command-line tools, edge functions, and anything with cold-start limits, size and startup reach users directly. For a long-running internal service they may never matter.
An executable that carries its runtime can be larger on disk and still be the easier deployment, since it does not depend on software installed on the host. I count moving parts in production before bytes in the repository.
4. No throw
Exceptions hide failure from the signature, so a caller has to read the implementation to learn which failures are expected. Good error design puts them in the type: callers handle the failure or pass it upward, and the compiler checks which. Error values, result types, and error unions all qualify, as long as one broad runtime error cannot swallow them all.
Handling failure has to be cheap as well, which takes a result or either type, an option type, and operators for chaining them. Real code is a pipeline of steps that can each fail, and I want to write it as transformations rather than a ladder of early returns and nested matches. When ten fallible calls need ten manual unwraps, people reach for the panic.
Panics belong to bugs such as an out-of-bounds index or an impossible state. The criterion is about expected failures: a missing file, a timeout, a rejected request, a permission error. Scripts and top-level handlers can let an error kill the process with a stack trace, which is useful output there. Libraries need a contract callers can compose.
5. Null safety
Null causes defects because it erases the difference between a value and its absence. A useful type system makes nullable and non-nullable values different types, so the compiler forces a check before use.
Absence belongs at boundaries: parsed input, database rows, optional configuration, API responses. Where the language cannot express the distinction I validate at the edge and keep nullable values out of the core. A linter catches some dereferences, but it cannot repair an API that treats null as valid everywhere.
Zero values cut initialization noise, but zero is a value and says nothing about whether absence is allowed. I want the type to answer that before the code runs.
6. Cross compile / multiplatform
Every target adds build and staffing work. If each one needs its own machine, each release depends on a CI matrix. If one machine produces all the artifacts, releases stay routine.
Test the real target list: operating systems, CPU architectures, system library variants, WebAssembly, mobile. Then runtime differences: path separators, case sensitivity, signals, line endings, text encodings. A build that succeeds does not prove a program that behaves the same way.
Native dependencies are where cross-compilation usually breaks. A pure source tree may cross-compile cleanly; a graph of system libraries and linkers may not. For desktop applications the GUI toolkit decides platform support more than the language does.
7. High-quality standard library
A strong standard library removes dependencies you would otherwise audit, pin, and eventually abandon. I want HTTP, JSON, cryptography, time, file I/O, and testing in the distribution, maintained with the language.
Coverage is only part of quality. I also look at API stability, documentation, security response, and whether behavior is predictable across platforms.
Expect gaps. The question is how many dependencies you need before the first feature works.
8. Big developer pool
Popularity is a practical asset even when it says nothing about language quality. A large pool makes hiring and onboarding easier, produces answers when something breaks, and gives AI tooling more training data.
The pool also sets the cost of being wrong. A niche language can suit a small team that owns its tooling; a large team on a deadline cannot absorb that risk. Look at the local job market and the people already on the team, then ask who will review the code after the original author leaves. A good choice for a solo project can be a bad one for a department.
9. Concise expressive language (low LOC)
Less code means less to read, review, test, and keep in sync. Pattern matching, sum types, first-class functions, and strong inference express an idea with less ceremony.
Lines of code is a poor target. Code golf produces dense code only the author can read, and a clever one-liner is often harder to review than several explicit statements. The useful question is how much ceremony a simple idea needs. Can a record and a transformation be written without a class hierarchy, a builder, and a factory?
I will take code where each line carries meaning over code that hides control flow to look short.
10. Ease of setting up the environment
The first developer experience is installation. A new contributor should clone the repository and run the build without a private wiki page.
Count the steps on a clean laptop and a clean CI runner. Is the toolchain one install? Does the project pin its version? Is there a lockfile? Does the build need system packages or environment variables the repository does not mention?
Setup friction returns with every hire, every CI change, and every machine replacement. Containers and reproducible builds reduce it and add a layer of their own. I weight this heavily for teams that replace machines often and for open source, where contributors arrive with unknown environments.
11. One way of doing things
A canonical toolchain removes a category of decisions. When the language ships an official formatter, linter, test runner, build tool, and package manager, reviewers discuss behavior instead of style, and the formatter picks tabs or spaces so that argument ends.
Every developer has opinions about tooling, and without defaults those opinions become recurring arguments. One formatter, one type checker, one docs generator, one package manager, and no proposal to consider: the option to disagree is gone. With it, each new developer arrives with a preferred tool and a reason to migrate, and every migration spends attention on work that does not ship.
Choice has value, but each tool adds a config file and a version to pin, again and again. The test is whether a new contributor runs one command to format, check, test, and build, and whether the community uses that command. An official tool nobody uses is worse than none, because it creates two standards.
12. Less fragmented ecosystem
The previous criterion was tooling that ships with the language. This one is the library market: how many competing ways exist to build a web application, an ORM, a command-line parser, or a test double?
Competition improves libraries, and a young language will have a messy market. The cost arrives later as abandoned packages, upgrade treadmills, security patches across several stacks, and decisions that eat time without improving the product.
Fewer choices mean more shared assumptions and better documentation for the common path. I look for a default that has survived several years, has a maintenance plan, and serves more than one large project. A library with one maintainer and no succession plan is a liability whatever its API.
13. Abstraction level
Low-level languages make you manage memory, build details, and platform differences. High-level languages let you describe intent and leave machine details to a runtime. Most business software is limited by developer time and I/O, not CPU, so garbage collection, a real string type, and an HTTP stack remove incidental work.
Higher abstraction has costs too: ORMs hide query cost, frameworks hide control flow, runtimes hide memory use and startup time. I want a high-level default with a documented way down when profiling justifies it. Which level that is depends on the domain, since embedded work needs precise control while a CRUD service benefits from defaults.
Which abstractions exist is a separate question. Sum types, product types, classes, inheritance, monadic constructs, type classes, and generics let you write the domain model down directly. Without sum types you get string flags and boolean pairs; without generics you get duplication or any. Too few abstractions make simple ideas verbose.
Too many bring a different failure. A language where every problem yields to a new type class, a deeper inheritance tree, or another monad invites abstractions only their author understands, and reviews turn into decoding sessions. I want a language whose abstractions cover the shapes that occur in real code, and a community that reaches for them sparingly.
Testing a shortlist
Once two candidates remain, build the same small slice in both: a database call, an HTTP endpoint, a test, a release build. Measure the cold build on a clean machine, the artifact size, the setup time, and the code required. Then ask someone who did not write it to make a small change.
That last step shows more than any benchmark. A language that looks pleasant in a greenfield example gets tiring when a new contributor has to find the right command, reproduce the environment, and read the error messages.
Language choice is hard to reverse and migrations are expensive. The practical question is what will still be reasonable in five years for the people who inherit the code. A good choice installs without drama, builds fast enough to keep feedback tight, makes failure visible, and has people around who can read it. None of that is glamorous, which is why it gets skipped in a debate about syntax. Writing it down first makes the tradeoffs explicit, and that is the most a checklist can do.