The Vocabulary of Runtime Choice

For nvm-sh/nvm, the specific problem here is not how to run a shell command but how to interpret the many different descriptions a user may give for a Node.js runtime. This chapter follows that interpretation—from an exact or partial version to an alias, LTS label, or project-local .nvmrc—so the reader can predict when two descriptions mean the same installed runtime, when they depend on changing metadata, and when nvm should stop rather than guess.

A version description is a request. The request may be precise, such as a complete version number, or deliberately flexible, such as “the latest release in this line.” Nvm’s job is to turn that request into one concrete version that can be located locally or selected for acquisition. Treating every selector as if it were already that concrete version is the source of many confusing results.

A selector is a request, not a version

An exact version is the easiest case. If a user names a complete version, nvm can look for that version in its installed trees and, when an operation permits network access, ask the configured release source whether it can be obtained. A partial version is already more interesting. A request such as a major or major-minor line expresses a constraint, not a single immutable object. Nvm can resolve it to the most appropriate matching version available in the relevant local or remote set.

That flexibility is useful because Node.js releases are continuous. A developer who wants the current release in a maintained line does not need to edit a command every time a patch release appears. The cost is that the same text can resolve to a different concrete version later. A partial selector therefore communicates a policy—“stay within this line”—rather than a historical fact—“use precisely the bits that were available today.”

Nvm also has names that are not ordinary version ranges. The built-in node name means the latest available Node.js release, while iojs refers to the latest io.js release. stable is a legacy alias with historical semantics; after Node.js moved to a stable release model, nvm treats it as an alias for node. unstable points to the old pre-1.0 notion of an unstable Node release. These names are convenient, but their meaning comes from nvm’s compatibility vocabulary rather than from the spelling of a semantic version.

Two other names describe the surrounding environment rather than the newest release. system means the Node.js or io.js installation supplied outside nvm, if one exists. current means the version that the current shell would find through its active PATH. That distinction is important. system asks to leave nvm’s selected path and use an external installation; current asks nvm to refer to the selection already active in this shell. Neither name is a promise about what a project file will request.

Custom aliases add a user-defined layer. An alias can point at a version or at another alias, allowing a meaningful label to stand for a team convention or a local workflow. Nvm stores these aliases as small pieces of state and follows the chain when resolving them. That makes aliases readable, but it also means the final answer is not determined by the alias name alone. The target can be changed, removed, or made unavailable, and a chain can become circular.

LTS labels sit between a human release policy and a concrete version. Node.js publishes long-term-support lines with codenames, and nvm exposes the latest LTS through lts/* and named lines through forms such as lts/<name>. To maintain those labels, nvm reads release metadata from the configured Node.js source and creates local LTS aliases. The label is therefore useful shorthand, but it is still a moving pointer. It should not be confused with a cryptographic pin or with a copy of the release itself.

The difference among these forms can be summarized as a question of what is allowed to vary:

  • A complete version varies least: the request identifies one release, subject to whether it is installed or obtainable.
  • A partial version varies within a release line.
  • A custom alias varies when its target file changes.
  • node and LTS labels vary as the relevant release metadata advances.
  • system varies with the external installation currently visible to the shell.
  • current follows the active shell selection rather than the project’s preferred description.

This is not a ranking of “good” and “bad” selectors. A team may intentionally want a moving LTS pointer, while a release job may require a complete version. The first decision is to state how much movement the context can tolerate. Nvm then supplies the vocabulary for expressing that decision.

The resolution path also depends on the operation. A command that only needs to describe installed versions can search local state. A command that needs the latest remote release must consult release metadata. A selector such as node may therefore have one answer when nvm is asked to inspect installed versions and another when it is asked to identify what can be downloaded. The label has not changed; the candidate set has.

That difference explains why “latest” is not a property stored permanently in the selector. It is a result produced by resolving the selector against a particular set of versions at a particular time. The shell-local control plane introduced earlier provides the place where the result becomes active, but it does not eliminate the changing input from which the result was derived.

It is useful to imagine resolution as a filter followed by a choice. The selector first narrows the candidate set: a major line excludes other majors, an LTS name excludes releases outside that support line, and an alias replaces a human label with its target. Nvm then chooses a matching result from the state relevant to the operation. Local inspection can only find versions already installed; remote inspection can consult the release index; installation may use the remote answer to decide what it should acquire. A valid selector can therefore produce “not available locally” without being an invalid selector.

That distinction is why the same wording can lead to different next steps. If a user asks which local version matches a partial request and none is installed, nvm has learned something precise: the request is understood, but the local candidate set is empty. If the operation is allowed to consult remote metadata, the candidate set may become nonempty. If network access is unavailable, the answer depends on installed and cached state instead. These are different failure surfaces, and treating them as one “version error” hides the difference between a bad request and a missing artifact.

Resolution also has a temporal boundary. A custom alias file is local state, while an LTS label is refreshed from release metadata. A project can pin an alias target to a concrete version and still depend on that version remaining installed; it can instead name an LTS line and accept movement as the maintenance policy. Neither choice is universally more correct. The right selector is the one whose allowed variation matches the consequence of being wrong.

Project context, aliases, and safe failure

A project directory can supply version intent without putting a command in every developer’s memory. When nvm use, nvm install, or nvm which receives no version argument, nvm searches the current directory and then its parents for a file named .nvmrc. The nearest file found in that upward walk provides the description to resolve. A command run deep inside a project can therefore inherit the project’s runtime policy from its directory context.

Suppose a repository contains an .nvmrc with lts/*. A command in a nested directory does not read that text as an exact version. It finds the file, removes comments and irrelevant whitespace, interprets the remaining selector, and asks the normal resolver to determine the applicable LTS version. If the file instead contains a complete version, the same path supplies a more stable request. The file changes the source of intent; it does not change the meaning of the selector placed inside it.

The parser is intentionally stricter than “take the first line and hope.” After comments and blank lines are ignored, a valid file must contain one bare nvm-recognized version-like value. The format also permits distinct key/value entries reserved for future use, but it rejects duplicate keys, a node key, and combinations with more than one bare value. Leading and trailing whitespace are not supposed to turn an otherwise understandable file into a different version. These rules give a project convention a clear failure mode instead of allowing two competing requests to be silently merged.

That strictness is easiest to appreciate in a nested workspace. Imagine a repository root with an .nvmrc, a documentation subdirectory, and a tools subdirectory several levels below it. A command launched from the tools directory can inherit the root’s selector even though the user never named it on the command line. If a second .nvmrc appears in an intermediate directory, the nearer file becomes the relevant context. The result follows directory proximity, not a global project registry. This makes local conventions convenient, but it also makes an unexpected parent file worth checking when a selection seems surprising.

The file’s contents still pass through the same resolver as an explicit argument. Comments are not alternate configuration, whitespace is not a second version, and a key/value extension does not turn the file into a general-purpose manifest. The one bare version-like value remains the required anchor. In practice, this means a team can add explanatory comments without changing the selector, while a malformed attempt to record two competing runtimes fails visibly instead of allowing different tools to choose different lines.

That failure behavior matters when directory context is inherited. A file in a parent directory can affect commands in many child directories, so an invalid or surprising file should not be treated as a harmless local typo. Nvm reports invalid content and leaves the selection unresolved. The same principle applies when a requested alias leads back to an alias already seen in the chain. Rather than choosing an arbitrary point in the loop, nvm represents the result as an infinite alias path and commands such as nvm use abort.

The following model shows the main sources of intent and the point at which they converge:

flowchart TD A["Explicit version argument"] --> R["Interpret selector"] B["Custom or built-in alias"] --> R C["LTS label and release metadata"] --> R D[".nvmrc found by upward search"] --> R R --> E["Concrete version result"] R --> F["Invalid selector or .nvmrc"] R --> G["Alias cycle"] E --> H["Use local state or continue to acquisition"] F --> I["Report failure without selecting"] G --> I

The diagram’s convergence should not be read as a claim that all inputs have equal priority in every command. An explicit argument is normally the clearest local instruction; a .nvmrc is consulted when an operation permits the directory to supply the missing version; LTS information is used when the selector asks for it. The point is that each route produces a description for the same resolution machinery, and each can terminate before a concrete version exists.

current deserves special care in this context. A project’s .nvmrc can name a desired version, but it does not redefine what the current shell is already using. This lets a script distinguish “run under the project’s declared context” from “continue with whatever runtime is active.” If a command needs the latter, passing current makes that intent visible instead of relying on an implicit interaction between PATH and a directory file.

Nvm’s behavior for run and exec makes the same distinction visible in a less strict form. These commands consult an explicit selector or .nvmrc, but the current implementation warns and falls back to the active Node.js version when neither produces a resolvable result. The project documents that fallback as provisional or undefined for predictable scripting. It is therefore a compatibility behavior, not a sound substitute for declaring the intended runtime. An automation script should use an explicit version, a deliberate LTS selector, or current when it truly means the active selection.

This is the boundary between convenience and reproducibility. A symbolic .nvmrc is valuable because it lets a project express a policy once and lets nvm follow the release line. It is not a lockfile: the same label can lead to a new concrete version as metadata changes. A complete version in .nvmrc narrows that movement, but it still presumes that the version can be installed on the host and that the runtime’s dependencies remain usable. The selector controls intent; it cannot erase the constraints of the artifact or the platform.

The same reasoning applies to custom aliases. An alias called team-default may make a command legible, but its stability depends on who can change its target and whether the target remains installed. An LTS name may express a shared maintenance policy, but its result depends on release metadata and the state nvm has learned about that line. A partial version can keep a project on a compatible family, but it delegates patch selection to the available candidate set. Naming the selector is therefore a small form of operational design: it declares where change is acceptable and where it is not.

For a reader diagnosing an unexpected result, four questions usually locate the issue. First, what text did the command actually receive—an argument, an alias, an LTS label, or a file-derived value? Second, which candidate set was consulted: installed versions, cached state, or remote metadata? Third, did the resolver produce a concrete version, or did it stop on invalid content or a cycle? Finally, was the command asking for the project’s version or for current? These questions are more reliable than starting with the assumption that nvm has selected the newest version.

Once a description has become a concrete version, the vocabulary has done its job. The next problem is acquisition: determining whether an existing installation or cache can satisfy that result, whether a remote artifact must be retrieved, and whether the resulting tree is usable. Keeping those stages separate prevents a failed download from being mistaken for a bad selector—and prevents a convenient label from being mistaken for the installed runtime itself.

References