Both ecosystems let you declare a range and then freeze the result in a lockfile, but the syntax and defaults differ. RubyGems uses the pessimistic operator and does not enforce semantic versioning. npm uses caret and tilde ranges. In both, the lockfile, not the range, decides what you actually install.
What is semantic versioning, and who follows it?
The Semantic Versioning specification defines MAJOR.MINOR.PATCH. Raise MAJOR for incompatible API changes, MINOR for backward-compatible features, PATCH for backward-compatible fixes. Version 0.y.z is for initial development, where "anything may change at any time." A pre-release, marked with a hyphen, ranks below its normal version.
Following the rule is up to each author. The RubyGems guide says outright that RubyGems does not enforce SemVer and attaches no meaning to the parts of a version. It also points out that Ruby itself, Ubuntu and Python use other schemes. The only reliable compatibility check, it says, is running your own tests against the new version.
How does the Ruby pessimistic operator work?
The ~> operator sets a lower bound and an upper bound at the next release the scheme calls incompatible. The guide's example: ~> 2.2 means at least 2.2 and below 3.0. So ~> 8.1 allows 8.x from 8.1 up and stops before 9.0.
The guide adds a rule for gem authors. In a gemspec, avoid ~> unless you have a specific reason, because a published gem requiring ~> 2.2 stops every application using it from moving to 3.x even when the upgrade would work. In an application Gemfile, the operator is a reasonable way to make major upgrades deliberate.
How do npm ranges work?
The npm package.json documentation lists the operators: exact versions, comparison operators, tilde described as "approximately equivalent to version", caret described as "compatible with version", x-ranges such as 1.2.x, hyphen ranges, and || for alternatives. The page also allows tags like latest, git URLs, local paths and aliases as dependency values.
The npm page does not spell out the caret and tilde bounds, so check the semver specification that npm uses for precise rules before relying on them. Likewise, it does not describe prerelease matching.
How do prereleases behave?
RubyGems treats any version with a letter as a prerelease, such as 2.0.0.rc1. Installers skip them by default, and Bundler considers one only when a requirement names a prerelease explicitly. Prereleases sort before their release, and a hyphen is read as .pre..
Constructed example: a Gemfile asks for >= 1.0.0 and a release candidate 1.1.0.rc1 exists. Bundler will not select it, because the requirement does not name a prerelease.
Where do lockfiles come in?
In Ruby, Gemfile.lock lets you choose when to upgrade, according to the RubyGems guide. For npm, the package.json documentation states that package-lock.json is ignored when packing a published package by default, and that npm-shrinkwrap.json is the way to publish a lockfile. The practical meaning: a library author's lockfile does not reach consumers, so consumers resolve ranges themselves.
How to check or apply this
- Commit the lockfile for every application, in both ecosystems.
- Read each range for its upper bound. List any
*, empty orlatestentries and replace them. - In gemspecs, use lower bounds unless you know of a break.
- Update dependencies on a schedule, in a branch, and run tests before merging.
- Treat 0.y.z packages as unstable and read their changelogs.
- Check the resolved versions, not the declared ranges, against vulnerability advisories.
Where does Safeguard fit?
Safeguard dependency scanning reads resolved versions and reports known vulnerabilities, with reachability analysis showing whether your code calls the vulnerable function.
Quick check
What does ~> 2.2 mean in a Gemfile?
At least 2.2 and below 3.0, per the RubyGems versioning guide.
Does RubyGems enforce semantic versioning?
No. It accepts any version and attaches no meaning to its parts, so run your tests on every upgrade.
Sources
- Semantic Versioning 2.0.0
- RubyGems Guides, Versioning and compatibility
- npm Docs, package.json dependencies
Last reviewed: 18 October 2026.