^0.2.3 matches >=0.2.3 <0.3.0: 0.2.3 and later 0.2.x patches, but not 0.3.0. Below 1.0.0 the caret keeps the leftmost non-zero number fixed, which here is the minor version, so ^0.2.3 behaves exactly like ~0.2.3.
Against real versions
0.2.2 no below the floor
0.2.3 yes
0.2.4 yes
0.2.9 yes the highest match
0.3.0 no treated as a breaking release
1.0.0 no
Why 0.x works differently
SemVer 2.0.0 (item 4) says major version zero is for initial development: anything may change at any time, and the public API shouldn’t be considered stable. node-semver, the range engine npm uses, follows common practice instead: in 0.y.z, a bump of y may break things and a bump of z shouldn’t. So for a caret range, 0.3.0 is as far from 0.2.3 as 2.0.0 is from 1.2.3.
The partial forms follow the same rule:
^0.2 >=0.2.0 <0.3.0
^0.0 <0.1.0 any 0.0.x
^0 <1.0.0 any 0.x
^0.0.3 >=0.0.3 <0.0.4
Gotchas
- Moving to 0.3.0 is up to you.
npm updatestays inside the range, so it never installs 0.3.0. Runnpm install pkg@^0.3.0when you’re ready, and read the changelog first: under this rule 0.3.0 may break. - 1.0.0 widens the range you’ll write next. Once the package ships 1.0.0,
npm install pkg@latestsaves^1.0.0, which takes every 1.x minor release. An existing^0.2.3never reaches 1.0.0. - The lockfile still pins one version.
^0.2.3only bounds what a fresh resolve ornpm updatemay choose;package-lock.jsonkeeps whatever was installed, andnpm ciinstalls exactly that.
Other ecosystems
Cargo reads it the same way: ^0.2.3, or the plain "0.2.3" that Cargo treats as a caret, is >=0.2.3, <0.3.0. Cargo’s docs call two versions compatible when their leftmost non-zero component is the same, and note that this differs from SemVer, which treats every pre-1.0.0 release as incompatible. Composer’s docs give the same reading for ^0.3: >=0.3.0 <0.4.0.