首页·报错信息

报错信息逐条解释

104 条报错信息,逐条讲清含义、起因和处理办法。

修法都有代价。git reset --hard 会丢掉没提交的工作,强制推送可能毁掉同事的提交,docker system prune 会删掉未命名的卷。凡是这样的条目,里面都写明了。

Git29

git 的错误几乎都是一种拒绝,意思是「在当前状态下做那件事会丢掉东西」;fatal: 表示它什么都没改就停了,而真正的解决办法往往在第一行下面的 hint: 行里。

fatal: refusing to merge unrelated histories你要合并的两边没有任何共同祖先,在 git 看来它们是两段互不相干的历史。这通常发生在你本地用 git init 开始、提交了几次,然后把一个已经有提交的远端加为 origin 再 pull 的时候。加上 --allow-unrelated-histories 确实能合并,但它把两段无关历史硬焊在一个提交上,以后很难拆开;如果本地只有几个提交,重新 clone 远端再把文件搬进去更干净。Your branch and 'origin/main' have diverged你的分支和 origin/main 各自都有对方没有的提交,也就是历史分成了两条。这发生在你本地提交的同时别人往远端 push 了,或者你用 amend、rebase 改写了已经推上去的提交。git pull --rebase 把你的提交重新叠到对方之上、让历史保持一条线,但会给你的提交换上新哈希,所以如果这些提交已经和别人共享过,同一份工作就会留下两遍;这种时候用 --no-rebase 留一个合并提交更稳。fatal: Need to specify how to reconcile divergent branches.从 git 2.34 起,分支分岔时 pull 不再替你猜是要合并还是 rebase,它在要一个配置,而不是在报告损坏。当两条分支已经分岔而你从未设过 pull.rebase 时就会看到它。pull.ff only 最稳妥,因为它只在能快进时才拉取,否则什么都不建就停下;pull.rebase true 每次都会改写你本地提交的哈希,而 false 会留下一个合并提交。CONFLICT (content): Merge conflict in src/app.tsx两边把同样的行改成了不同的样子,git 无法判断哪个对,于是把两个版本都写进文件里,夹在 <<<<<<<、======= 和 >>>>>>> 之间,然后停下。这发生在你和别人改动了同一段,或者 rebase 把你的提交挪过了对这几行的修改。打开文件改成该有的样子,删掉这些标记,然后 git add 再 commit 就完了。git merge --abort 会精确回到合并之前的状态,对已提交的东西没有风险,但你手工解决过的部分会一起丢掉。Please commit your changes or stash them before you merge.你有未提交改动的文件,正好也是即将拉进来的提交要改的文件,git 选择停下而不是覆盖它们。它出现在工作区不干净时执行 pull 或 merge。git stash push -u 把这些改动先挪到一边让合并通过,-u 会连未跟踪的文件一起收;但 git stash pop 取回时仍可能冲突,而且 stash 不像分支那样看得见,很容易忘掉丢掉,所以随手做一个临时提交是更稳的习惯。error: Your local changes to the following files would be overwritten by checkout:你想切到另一个分支,但那边内容不同的文件里还留着你没提交的改动。它出现在活干了一半就 switch 或 checkout 的时候。git stash push -u 或者随手一个临时提交能让你过去,之后还能回来。搜索里常见的 git checkout --force 和 git switch --discard-changes 确实能切过去,但会永久删掉那些未提交的改动——连 reflog 都不会留下记录,没有任何找回的办法。You are currently rebasing branch 'feature' on '8a3f21c'.一次 rebase 开始后停在了中途,HEAD 停在一个临时状态上,而不是你的分支上。你会在 rebase 期间发生冲突之后,或者 git rebase -i 在 edit、break 处停下之后看到这一行——你没把它做完就去干别的了。解决冲突后 git add,再用 git rebase --continue 继续;如果打算丢掉那个提交就用 --skip。git rebase --abort 会精确回到 rebase 开始之前的位置,已提交的东西一点不丢,但你到目前为止手工解决的冲突会一起消失。You are in 'detached HEAD' state.HEAD 直接指向某个提交而不是分支名,所以你在这里做的任何提交都不会挂上名字。切到某个提交哈希或标签、在子模块里干活、或者 CI 检出了某个特定提交,都会进入这个状态。想留住这里做的活,git switch -c 名字 就地建一个分支,什么都不会丢。如果你直接 git switch main 离开,你刚做的提交就没有任何东西指向它:reflog 还能找一段时间,等垃圾回收跑过就没了。Warning: you are leaving 1 commit behind, not connected to any of your branches:它在警告:你在 detached HEAD 状态下做了提交,现在要离开,而没有任何分支指向那些提交。这发生在你切到某个提交哈希、干活、提交,然后又回到分支的时候。直接用消息里打出来的哈希:git branch rescue 8a3f21c 就把它们钉在一个名字上,这条命令别的什么都不改,只加一个名字。如果你已经离开了,git reflog 里还列着它们,但没有任何东西指向的提交会在垃圾回收跑过后被清掉——默认大约三十天。error: failed to push some refs to 'https://github.com/user/repo.git'服务器拒绝了这次 push,这一行只是结果摘要——真正的原因写在它上面那行 ! [rejected] 里。多数情况是远端有你没有的提交,但受保护分支、服务端钩子、文件大小限制也会给出同样的摘要。先把上面的原因读掉;普通情形下 git pull --rebase && git push 就结束了。最危险的动作是不读原因就加 --force,那可能把同事在服务器上的提交抹掉。! [rejected] main -> main (fetch first)远端分支上有你的克隆从没见过的提交,所以现在 push 就不是快进了。这发生在同事先 push 了,或者你上次 fetch 已经很久之前。git pull --rebase origin main 把那些提交拉进来、再把你的叠上去,然后 push 就能过。这里绝对不要用 --force:你会覆盖的提交是别人的,而且根本不在你的仓库里,本地任何东西都找不回来。! [rejected] main -> main (non-fast-forward)你的分支不是远端分支的后代,照原样推上去会让服务器上已有的提交从历史里掉出去。这发生在你用 rebase、amend、reset 改写了已经推上去的提交。如果改写是有意的、而且这条分支只有你在用,就用 git push --force-with-lease:只要你上次 fetch 之后远端动过,它就会拒绝,所以和光秃秃的 --force 不同,它不会悄悄抹掉这期间同事推上来的东西。如果改写不是有意的,答案是 pull --rebase,而不是强推。Updates were rejected because the remote contains work that you do not have locally.这是 push 被拒的原因说明:远端有你的克隆没有的工作,现在推上去会把它删掉。它出现在多人共用一条分支时,或者你在网页上改文件提交后一直没拉下来。先 git pull --rebase origin main,再 push,就顺利通过了。不理这条提示而加 --force,正好做了提示要防的事:别人的提交从服务器上消失,而如果他 push 之后没人 fetch 过,这些提交就哪里都不剩了。error: src refspec main does not match any你让 git 推的那个名字,在本地既不是分支也不是标签,不存在的东西没法送出去。这发生在刚建的仓库还没有任何提交、main 这个名字还没诞生的时候;或者本地分支叫 master 而你打的是 main;又或者只是打错字。git push -u origin HEAD 会把你实际站着的那条分支按它真正的名字推上去,一步就绕过名字对不上的情况。如果一个提交都没有,先做一个——空仓库没有可送的东西。fatal: The current branch feature has no upstream branch.这条分支没有登记对应的远端分支,所以不带参数的 git push 或 git pull 不知道该去哪里。它出现在你用 git switch -c 新建分支后从未推送过的时候。git push --set-upstream origin feature 会在服务器上建出这条分支,并把对应关系登记一次,此后只打 git push 就够了。这条命令只写一行本地配置和服务器上一条新分支,什么都不删,重复执行也安全。error: cannot lock ref 'refs/remotes/origin/main': is at 8a3f21c but expected 1c2d3e4git 正要往一个远程跟踪引用写入新值时,发现里面的值和它刚读到的不一样,于是拒绝覆盖。常见于编辑器在后台 fetch 而你同时也在 fetch,或者有人做了 force push、你的远程跟踪引用错位了。多数情况下再 fetch 一次就过去了。git remote prune origin 会清掉上游已经不存在的分支所对应的远程跟踪引用;它删的只是你仓库内部的副本,绝不碰服务器上的分支。fatal: not a git repository (or any of the parent directories): .git当前目录以及它上面的任何目录里都没有 .git,git 找不到仓库。这发生在你站在仓库上一层或下一层、clone 落到了别的文件夹、或者你从来没跑过 git init 的时候。先用 pwd 确认自己在哪,只有真的想在这里开一个仓库时才用 git init。在已有仓库的子文件夹里跑 git init 会在里面生出第二个嵌套仓库,悄悄把外面那个遮住,从那以后这个文件夹里的文件再也不会进入外层的提交。fatal: remote origin already exists.在这个仓库里 origin 这个名字已经被占用了,不能再建一个同名的。clone 下来的仓库一开始就有 origin,而这条消息出现在你在其中照着「新建仓库」的教程执行 git remote add origin 的时候。先用 git remote -v 看看 origin 指向哪里;如果你想换的是地址,git remote set-url origin 只改指向。这个设置是 .git/config 里的一行,随时可以改回来,对你的提交没有任何影响。error: pathspec 'featue' did not match any file(s) known to gitgit 没有找到叫这个名字的文件、分支或标签——引号里的那串字,就是它实际去找的名字。这可能是打错字,或者那条分支只在服务器上而你还没 fetch,又或者文件不在你以为的那个目录里。是分支就先 git fetch 再用 git branch -a 把名字对着看;是文件就用 git status --short 看真实路径。要记得 git 命令里的路径是相对你当前所在目录解析的,不是相对仓库根,光这一点就能解释其中很多情况。git@github.com: Permission denied (publickey).ssh 已经连到了服务器,但没能拿出任何服务器愿意接受的密钥——这是认证问题,不是网络问题。它出现在你还没生成密钥、生成了却没加进 ssh-agent、或者公钥没有登记到该服务的账号上的时候。ssh-add ~/.ssh/id_ed25519 会在本次会话里把密钥装进 agent,而 ssh -T git@github.com 会告诉你试了哪把密钥、你被认成了谁。两条命令都只读,不会有任何损失。remote: Support for password authentication was removed on August 13, 2021.GitHub 在 2021 年停止接受通过 https 的账号密码——不是你的密码错了,而是这种方式本身不再被接受。它出现在你保存的凭据是旧密码,或者你在提示处直接输入了密码的时候。想继续用 https,就在密码栏里填 personal access token;否则用 git remote set-url origin 换成 ssh 地址。改远端地址只是你自己克隆里的一行配置,不动任何提交,随时可以改回来。fatal: Authentication failed for 'https://github.com/user/repo.git/'送出去的凭据被拒了——它是错的、过期的,或者是一个没有这个仓库所需权限范围的 token。常见的坑是过期 token 还留在凭据助手的缓存里:它连问都不问就被送出去,所以每次都是同一行。修复里那条 git credential reject 只删掉那一条保存的记录,让系统重新向你询问,你就能贴上新的 token。它只删已保存的凭据,不动仓库的任何数据。fatal: Unable to create '/repo/.git/index.lock': File exists.git 写索引时会建 .git/index.lock 当锁,所以这个文件已经存在,意味着另一个 git 正在跑,或者有一个死掉时把它留下了。它常被后台跑 git 的编辑器或 IDE 留下,也可能是你用 Ctrl+C 打断或杀掉的命令留下的。先确认没有 git 在跑,再用 rm -f .git/index.lock 删掉。在真有 git 进程干活时删它可能弄坏索引,所以务必先确认;万一索引乱了,git reset 能从 HEAD 重建它,不加 --hard 就不会动你的文件。nothing to commit, working tree clean这不是错误:git 在告诉你,和上一个提交相比没有任何差异,所以它什么也没做。你会在改的文件被 .gitignore 命中、你编辑的地方其实是另一个克隆或 worktree、或者你已经提交过却忘了的时候看到这一行。git check-ignore -v <路径> 会打印出遮住这个文件的那条 .gitignore 规则本身,而 git log -1 能确认这次改动是不是已经进去了。两条命令都只读。error: unable to unlink old 'dist/main.js': Permission deniedcheckout 想把一个文件换成新版本,而操作系统不让删掉旧的。这发生在别的程序把这个文件开着(Windows 上尤其常见),或者你对这个目录没有写权限的时候。把占着它的东西关掉——开发服务器、编辑器、杀毒软件——再跑一遍同样的命令;在 Unix 上则修正目录权限。重复这条命令是安全的,但要注意 git 是停在中途的,所以在成功之前工作区一直处于只更新了一半的状态。fatal: bad object 8a3f21c你给的名字解析不到任何 git 读得懂的东西:要么这个对象不在这个仓库里,要么在里面但已经损坏。这发生在哈希是从别的克隆或浅克隆里抄来的、哈希被截断得太短、或者磁盘出问题后对象真的坏了的时候。git fsck --full 只读地报告缺失和损坏的对象,而别人给的哈希要先 git fetch,那个对象才会到你这里。如果 fsck 真报出损坏,重新 clone 比修补更快也更可靠——只要先把未提交的文件抄到安全的地方。warning: LF will be replaced by CRLF in package.json.这是提示,不是错误:core.autocrlf 是开着的,所以 git 在提交里存 LF,却会往你的工作副本里写 CRLF。在 Windows 上按默认值安装 git 会把 autocrlf 设成 true,所以那台机器上每次 add 都会看到它。git config core.autocrlf input 会在提交里存 LF、取出时不再转换;更好的办法是在仓库里放一个 .gitattributes,写上 * text=auto eol=lf,让每个克隆行为一致。改这个设置可能让所有文件看起来都被改过一次;那只是一次重新取出,不是丢了工作。The file will have its original line endings in your working directory这是上面那条 CRLF 提示的后半句:它说磁盘上的文件保留它现有的行尾,只有存进提交的那份副本被规范化。它来自同一个 autocrlf 设置,本身没有任何东西坏掉。但如果 diff 里整个文件都显示被改过,那就是团队里行尾不一致的信号:在仓库里放一个 .gitattributes 把规则钉住,再跑一次 git add --renormalize .。这条命令只改写行尾的存储方式,不改文件内容。husky - pre-commit hook exited with code 1 (error)你的提交根本没有生成:某个钩子——lint、测试、格式化——以非零码退出,git 中止了提交。真正的原因不在这一行,而在它上面钩子自己的输出里,通常是一条 lint 规则或类型错误。把钩子指出的问题改掉再提交,是唯一真正的解法。git commit --no-verify 会跳过所有提交钩子并确实生成提交,但它不是通过了检查,只是把失败推给了 CI,未格式化的代码会原样送到同事那里。

npm20

npm 的原因写在第一行 code XXXX 里,而不是最后那六行 npm ERR!;如果失败来自依赖树或原生编译而不是你自己的代码,删掉 node_modules 重新安装就能解决一半。

npm ERR! ERESOLVE unable to resolve dependency tree从 npm 7 起 peerDependencies 的版本范围会被强制执行,这句话是说 npm 找不到一组能同时满足所有范围的版本。常见于你要装的包的 peer 范围排除了你手上已有的 react 或 typescript 版本,尤其是刚做完一次大版本升级之后。读输出里的 Found: 和 Could not resolve: 两行,看清谁要求什么,然后把两者之一挪到兼容的版本——这才是真正的修法。npm install --legacy-peer-deps 会完全无视 peer 范围直接装上,能让你继续往下走,但留下的是一棵各个包从未认可的依赖树,版本不匹配带来的运行时错误得你自己去查。npm ERR! Conflicting peer dependency: react@18.3.1这一行从 ERESOLVE 的报告里挑出了真正冲突的那一对:这个包要求某个 peer 的那个版本,而你的依赖树里装的是另一个。通常意味着主库做了大版本跳跃,而用它的插件还没跟上。按名字问一句 npm ls react,就会以树的形式打印出谁要求了哪个版本,该动这两者中的哪一个当场就清楚了。把插件升上去才是真正的修法;--force 照样把不匹配的树写下去,把问题藏到运行时才爆。npm WARN react-dom@17.0.2 requires a peer of react@17.0.2 but none is installed.npm 6 只对 peer 依赖发警告,从来不替你装:包是装上了,但它说自己需要的那个伙伴没有。它出现在仍停留在 npm 6 的项目里,或者你在用那个年代留下的旧 lock 文件。按警告里印出的范围自己装上那个 peer 就完事了。现在还没有任何东西失败,因为这只是警告,但它稍后会以 Cannot find module 的形式回来,或者变成同一个库装了两份、报出莫名其妙的错。npm WARN deprecated request@2.88.2: request has been deprecated这个包的作者把那个版本标成了不再推荐:它装上了,也照样能跑。它通常不是你自己 package.json 里的东西,而是你在用的某个包顺带拖进来的间接依赖。按名字问一句 npm ls request,就能看到是你哪个直接依赖把它带进来的,要动的地方是那个直接依赖,而不是这个包本身。今天不用做任何事——deprecated 不是安全漏洞,安全漏洞是 npm audit 告诉你的。npm ERR! npm ci can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync.npm ci 只按 lock 文件里记录的东西装,而它连开始都没开始,因为 package.json 要的东西 lock 文件里没有。这发生在有人手改了 package.json、或在里面解了冲突却没跑 install,又或者只提交了 package.json 而没把 lock 文件一起提交。在本地跑一次 npm install 让 lock 文件对上,然后把 lock 文件提交——这就是全部的修法。为了绕过它而删掉 package-lock.json,会把所有依赖重新解析到更新的版本,悄悄改变你构建里的内容。npm ERR! The `npm ci` command can only install with an existing package-lock.json or npm-shrinkwrap.json没有 lock 文件,npm ci 根本不会运行,因为没有任何东西告诉它该装哪些版本。这发生在 package-lock.json 被 .gitignore 挡住、从未被提交过、或者你当前所在的目录不是项目根的时候。npm install --package-lock-only 会只生成 lock 文件而不装任何东西,把它提交上去,CI 就能跑了。千万不要把 package-lock.json 放进 .gitignore:CI 两次构建出同样东西的保证,全靠这一个文件。npm ERR! Invalid Version:package.json 里的 version 不是合法的 semver,所以 npm 根本没法解析这个包。它出现在手改成 1.0、v1.0.0 或空字符串这类值的时候,尤其是这个文件的冲突没解好之后。npm pkg set version=1.0.0 会写回一个规范的三段版本号。这条命令只改 package.json,既不动 node_modules 也不动 lock 文件,所以改完之后所有读这个文件的命令立刻恢复正常。npm ERR! 404 Not Found - GET https://registry.npmjs.org/@acme/ui - Not found注册表里没有这个名字的包——404 是关于名字的回答,不是关于你的网络或凭据的。它出现在名字打错、包被取消发布、或者你没登录的私有 scope 上。对没有权限的人来说,私有包看起来和不存在的包完全一样,所以这三种情况都挤在这一行里。npm view @acme/ui version 先确认这个名字是否公开存在;如果是私有 scope,检查 .npmrc 里有没有那个 scope 的注册表和 token。两项检查都只读。npm ERR! request to https://registry.npmjs.org/express failed, reason: unable to verify the first certificateTLS 连接失败了:服务器出示的证书链最终指向 node 不信任的机构。绝大多数情况是公司网络的代理把流量拆开、用自己的证书重新签名;偶尔是服务器没有把中间证书一起发过来。用 npm config set cafile 把公司的根证书告诉 npm,这才是正确的修法。strict-ssl false 也能让这行消失,但那是把证书校验整个关掉,路径上的任何人都能给你送来被改过的包——不要用。npm ERR! code EINTEGRITYnpm 下载下来的压缩包哈希和 lock 文件里记录的不一致,于是 npm 判定收到的东西不可信并停下。原因是缓存条目坏了、代理动过下载内容,或者更少见地,lock 文件里的 integrity 值被手改过或合并出了错。npm cache clean --force 会清空本地缓存,下次安装就重新下载;它只删缓存副本,重复执行也安全。如果只在某一个包上反复出现,那是记录的 integrity 本身就错了,需要用 npm install 重新解析那一项。npm ERR! Error: EACCES: permission denied, access '/usr/local/lib/node_modules'npm 想往一个不属于你这个用户的系统目录里写——包本身没问题,你只是没有在那儿写的权限。这几乎总是对着系统包管理器装在 /usr/local 之类位置的 node 执行 npm install -g。npm config set prefix ~/.npm-global 把全局安装挪到你的家目录,把它下面的 bin 加进 PATH,这个错就不会再来。sudo npm install -g 确实能装上,但会在缓存和 node_modules 里留下 root 所有的文件,日后普通安装照样报同一个 EACCES;用 nvm 这类版本管理器可以让整类问题消失。npm ERR! enoent ENOENT: no such file or directory, open '/home/me/package.json'npm 在当前目录以及往上一层层找 package.json,一个都没找到——你在一个不是项目的地方执行了 npm 命令。这发生在你站在 monorepo 的根上而项目在子文件夹里、你站在克隆下来的文件夹旁边、或者你干脆忘了 cd 的时候。答案是切到有 package.json 的那个文件夹,用 ls 看一眼最快。只有当你真的想在这里开一个新项目时才用 npm init -y:在错的文件夹里跑,会留下一个多余的 package.json,日后把工具搞糊涂。npm ERR! code ELIFECYCLEpackage.json 里的某个 script 以非零码退出了——ELIFECYCLE 是 npm 在外面包的一层壳,不是原因。原因是你的构建或测试脚本干了什么,真正的错误行在这一行的上面。往上翻到第一条错误,或者把 npm ERR! <pkg>@<ver> <script>: 后面打出来的那条真命令手动跑一遍,就能看到不带 npm 外壳的原始输出。后面跟着的退出码也能透露一点:1 是普通失败,137 表示进程被杀掉了,通常是因为内存。gyp ERR! build error某个依赖里含有 C 或 C++ 代码,安装时必须在你的机器上编译,而这次编译失败了——node-gyp 需要编译器和 python。这发生在完全没装编译工具链的时候,或者这个包比你的 node 老、头文件已经对不上的时候。装上工具链:macOS 上用 xcode-select --install 装命令行工具,Debian 或 Ubuntu 上装 build-essential 和 python3,Windows 上装 Visual Studio 的 C++ 工作负载。如果这个包的新版本为你的 node 提供了预编译的二进制,升级这个包比修编译环境快得多。Error: Cannot find module 'express'node 没能把这个名字解析到任何文件:它不在 node_modules 里,或者你是在一个没有 node_modules 的目录下运行的。这发生在你根本没装、装到一半失败留下半棵树、或者这个包在 devDependencies 里而你用 --omit=dev 装的时候。rm -rf node_modules && npm install 会从 lock 文件重建整棵树;它安全、什么都不丢,代价只是下载时间。但如果引号里的名字是以 ./ 开头的相对路径,那就不是少了包,而是你自己代码里的路径写错了。Module not found: Error: Can't resolve './components/Button' in '/app/src'打包器在那个路径下找不到那个文件——这是你自己写的 import,不是包的问题。它出现在相对路径差了一层、扩展名漏了或写错、或者文件名的大小写和 import 不一致的时候。最后一种最阴:macOS 和 Windows 的文件系统不区分大小写,所以在你机器上能跑,只在 Linux 的 CI 上崩。把那个目录 ls 出来,和 import 一个字符一个字符地对,大小写也要对。如果这个名字是包而不是路径,那就把它装上;两种情况下都不会删掉任何东西。Error: error:0308010C:digital envelope routines::unsupportednode 17 换上了 OpenSSL 3,把一个老的哈希算法从默认里去掉了,而仍然要用它的工具就在那次哈希调用里当场倒下。这几乎总是一个老的 webpack 4,或者内含 webpack 4 的工具,跑在现代 node 上。export NODE_OPTIONS=--openssl-legacy-provider 只为那个进程重新打开老算法,用于构建没有害处。但这是在硬撑一个已死的工具:升到 webpack 5,或者升到你框架的当前版本,这个选项就完全不需要了。FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryV8 的堆到了上限,node 自己放弃了——不是操作系统把它杀了,是 node 判断没法再长下去而停住。原因是一次大的类型检查或打包、在大项目上开着 source map 构建,或者像把所有东西都往数组里堆这样的泄漏代码。export NODE_OPTIONS=--max-old-space-size=4096 把上限提到 4 GB,往往就够了,但机器得真有这么多内存;如果容器的限制更低,这次换成操作系统来杀进程,表现为退出码 137。如果这个数字得一次次往上加,原因就是泄漏,而不是上限。npm WARN EBADENGINE Unsupported engine某个包在 engines 里声明的 node 或 npm 版本范围,你的环境不满足——npm 只是警告,照样装上。它出现在你用的是系统包管理器装的旧 node,或者项目已经迁到比你 shell 里更新的 node 的时候。用你的版本管理器把 node 切到打印出来的范围之内;该信的是项目的 .nvmrc 或 package.json 里的 engines 字段。默认它只是警告,但如果 .npmrc 里有 engine-strict=true,同样的条件就会变成中断安装的错误。zsh: command not found: tscshell 把 PATH 找了一遍,没找到叫这个名字的可执行文件——安装本身很可能是成功的。它出现在 npm 的全局 bin 目录不在 PATH 上、你装的是本地而不是全局所以可执行文件进了 node_modules/.bin、或者 shell 还缓存着旧的 PATH 的时候。npx tsc --version 完全不动 PATH 就能跑本地那份,所以这一行就能把「没装上」和「不在 PATH 上」分开。如果确实是全局安装,把 npm prefix -g 的输出后面加上 /bin 这个路径加进 PATH,然后重开一个 shell。

JavaScript15

浏览器和 node 的错误通常只告诉你什么坏了,不告诉你那个值为什么变成这样——你读了 undefined、它不是函数、它不是 JSON——所以该修的地方在抛错那一行的上游;只用 ?. 和默认值把那一行按住,同样的问题会在更远的地方、以更难辨认的样子回来。

Uncaught TypeError: Cannot read properties of undefined (reading 'name')点号左边的值是 undefined,你却去读它的属性;括号里的名字是你想读的属性,所以坏掉的是它前面那个值。它来自还没到达的响应、没人传进来的 prop、像 data.user.name 这样多探了一层,或者对空数组取 arr[0].id。如果这个值本来就可有可无,user?.name 能短路掉;但如果它本该存在,?. 只是把报错换成一个 undefined,推给下一行去炸——去上游找它为什么是 undefined。Chrome 78 之前同一个错误写作 Cannot read property 'name' of undefined。TypeError: items.map is not a function这个名字存在,但它不是函数——如果它压根不存在,消息会改成说 undefined。典型原因是:你当成数组的东西其实是对象或 NodeList;API 返回的是 {data: [...]} 而不是数组本身;或者模块默认导出的形状你猜错了。别先猜,先打印:一行 console.log(typeof items, items) 就能当场解决其中一半。是 NodeList 或 Set,Array.from(items) 包一下就好;但若它是 {data: [...]},包是错的,正确做法是 items = res.data。RangeError: Maximum call stack size exceeded调用堆叠的深度超过了引擎允许的上限,而这通常不是递归太深,是递归停不下来。终止条件缺失或永远到不了的函数、互相调用的两个函数、对包含自身的对象做 JSON.stringify,在 React 里则是渲染过程中修改了自己依赖的状态。控制台里的调用栈会反复出现那两三个帧——那一对就是环,基线条件该放在那里。如果这段计算真的需要深度,把递归改写成循环或显式栈是唯一的路:浏览器的栈上限是抬不高的。SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON你交给 JSON.parse 的是 HTML,不是 JSON。消息里引出的 <!DOCTYPE 片段就是证据,它说明服务器返回的是一个 HTML 页面——404、502,或者跳转登录页——所以真正的毛病在解析之前,在那次请求上。典型原因:地址写错、开发服务器的代理把 index.html 当成 API 返回了、会话过期被重定向到登录页。调 res.json() 之前先看 res.ok,不对就用 res.text() 读出到底来了什么;用 curl -s 直接打那个地址是最快的一眼。Chrome 111 和 Node 20 之前,同一个错误写作 Unexpected token < in JSON at position 0。SyntaxError: Unexpected end of JSON input解析器还在读 JSON,文档就结束了;几乎总是因为响应体本来就是空的。对 204 No Content 或没有响应体的错误响应调用 res.json(),把响应体读了两次而第二次拿到空,或者读了一个写到一半被截断的文件。先取文本、判断是否为空,再解析,才能得到准确的诊断——const t = await res.text(); if (!t) return null;。用 JSON.parse(text || 'null') 糊过去,会把空响应当成正常,所以先弄清服务器为什么发了空的响应体。TypeError: Failed to fetch请求结束时没有拿到任何响应,而浏览器故意隐去了原因:被 CORS 拦下、地址是死的、证书不对、广告拦截扩展掐断,都会打出同样这几个字。它是被抛出的错误而不是状态,所以 404、500 根本到不了这里——服务器连开始应答都没做到。控制台里这一行的上方或 Network 面板里,往往还有一条更具体的说明,先读它;用 curl -i 直接打那个地址,能把「服务器挂了」和「浏览器拒绝了」分开。Firefox 里同样的情形写作 NetworkError when attempting to fetch resource。Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.请求到达了服务器,响应也回来了,但那个响应里没有允许这个来源的头,于是浏览器拒绝把它交给你的 JavaScript。只有浏览器执行这条规则,所以同一个地址用 curl 就能通——看起来像是服务器没问题、只有浏览器坏了。要改的地方永远在服务器:给响应加上 Access-Control-Allow-Origin: http://localhost:3000。前端什么也做不了;如果服务器不是你的,经由自己的服务器转发就是唯一的路。Allow-Origin: * 对携带 cookie 的请求无效,那种情况必须写出确切来源,并同时打开 Allow-Credentials。SyntaxError: Cannot use import statement outside a modulenode 或浏览器把那个文件当作 CommonJS 脚本来读,而里面出现了 ESM 的关键字 import。除非 package.json 另有声明,.js 文件一律按 CommonJS 读;在浏览器里,则是 <script> 标签少了 type="module" 时出同样的事。npm pkg set type=module 把整个包声明为 ESM 就能解决,但从那一刻起这个包里所有 .js 都失去 require 和 __dirname,剩下的 CommonJS 文件必须一起迁移——如果只想改一个文件,把扩展名换成 .mjs 是更小、更安全的改动。ReferenceError: require is not defined in ES module scope, you can use import instead这正是上一条错误的镜像——文件被当作 ESM 读,里面却有 CommonJS 的 require。它几乎总是发生在刚给 package.json 加上 "type": "module"、而旧脚本还留在目录里的时候。想让那一个文件留在旧世界,mv script.js script.cjs 是最小的改动;想把它迁过去,把 require 改成 import 的同时还得处理 __dirname 和 require.main === module,这两个在 ESM 里并不存在。Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/src/util' imported from /app/src/index.jsNode 运行 ESM 时,像浏览器那样把 import 路径当作字面的文件名,所以 './util' 不等于 './util.js',而是一个不存在的文件。CommonJS 会替你试扩展名,而 TypeScript 编译后也原样保留不带扩展名的 import,所以用 tsc 构建的项目第一次用 node 跑起来时会成堆地报这个。给相对路径的 import 补上 .js 扩展名;在 TypeScript 文件里也照样写 './util.js',因为 Node 读到的是编译产物。来自 node_modules 的包名不加扩展名。ReferenceError: window is not defined这段代码是在服务器的 Node 里跑的,不是在浏览器里。Node 没有 window、没有 document、也没有 localStorage,所以在 Next.js、Nuxt 这类先做服务端渲染的框架里,模块加载阶段就去碰 window,就会停在这里——往往是组件外面的那一行,或者仅限浏览器的库的 import 本身。如果这活儿只属于浏览器,用 typeof window !== 'undefined' 包住,或者更好,把它搬进 useEffect,因为 effect 只在浏览器里运行。若整个库都只能在浏览器用,Next.js 里可以用 dynamic(() => import('./C'), { ssr: false }) 把它从服务端渲染里排除,代价是这部分不会出现在服务器生成的 HTML 中。Hydration failed because the initial UI does not match what was rendered on the server服务器发来的 HTML 与浏览器首次渲染出来的结果不一致,React 无法把两者对接。在渲染过程中用 new Date() 或 Math.random()、读 localStorage、按 window 宽度画不同的东西,都注定给出两个不同的答案;浏览器会悄悄纠正的标签也一样,比如 <p> 里套 <div>。规矩是:首次渲染要画出和服务器完全相同的东西,差异留到 useEffect 跑完之后再改。代价是那部分会晚一瞬间出现,并且不在服务器生成的 HTML 里。React 19 把同样的情形写作 "the server rendered HTML didn't match the client",还会打印出哪里不一致的 diff。Objects are not valid as a React child (found: object with keys {name, id})你交给 React 渲染的值是一个对象,而不是字符串或数字,括号里的键列表告诉你它是哪个对象。通常写着 {user} 的地方本该是 {user.name},或者你想把整个响应直接画出来。如果看到的不是键列表而是 found: [object Promise],原因就换了:你把一个 async 函数的结果没 await 就渲染了。解决办法是挑出要显示的字段;调试时用 JSON.stringify(user) 看一眼可以,但不要把它留在界面上。Each child in a list should have a unique "key" prop.作为列表渲染的兄弟元素没有 key,于是下一次渲染时 React 无法把它们和同一批数据对应起来。你用 items.map(...) 造元素时漏了 key;这只是警告,界面照样画得出来,但列表一变它就以无声的 bug 回来——症状是输入框的值或动画留在了错误的那一行。用数据里稳定的 id 作 key。用数组下标当 key,只有在顺序永不改变、也从不在中间插入或删除的列表里才安全,否则它复现的几乎就是「没有 key」的那个问题。从 React 19 起,前面的 Warning: 不再出现。Too many re-renders. React limits the number of renders to prevent an infinite loop.一次渲染改了状态,这个状态又触发了下一次渲染,React 为了不让它无限下去把这个环截断了。几乎总是「该传函数的地方把函数调用了」:onClick={setOpen(true)} 每次渲染都会立刻执行,正确写法是 onClick={() => setOpen(true)}。另外两种常见形态是在渲染函数体里直接改状态,以及 useEffect 的依赖数组里放了它自己会更新的值。把改状态的动作搬进事件处理函数或 useEffect;如果这个值渲染时算一下就有,不妨反问它是否需要成为状态。

Python18

Python 的 traceback 把答案劈成两半——最后一行说「错了什么」,上面的帧说「错在哪里」——所以只读最后一行,你会拿到名字却丢掉位置;而 NoneType、KeyError 这类「值不存在」的错误,原因几乎总在崩溃那一帧的上面。

ModuleNotFoundError: No module named 'requests'Python 把 sys.path 上的目录都翻了一遍,没有这个名字的模块。要么根本没装,要么装到了另一个解释器里——在虚拟环境外面 pip install、又在里面运行,必然是这个结果。写成 python -m pip install 会装进你真正在跑的那个解释器,这个错位就消失了。error: externally-managed-environment这个 Python 是操作系统或 Homebrew 管着的,pip 拒绝往里面写(PEP 668)。你是对系统解释器执行了 pip install;这条消息是在让你建虚拟环境。--break-system-packages 就像它名字说的那样,可能让 apt 管理的文件处于损坏状态,所以正确做法是建一个 .venv 并装在里面。ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import)两个模块互相 import,于是你从一个只加载了一半的模块里取名字。它出自 A 引 B、B 又引 A 的结构,在刚拆分文件、某个类型标注把 import 又拉回去时最常见。把 from y import Thing 改成 import y,在函数里写 y.Thing:查找推迟到调用时,这个环就不再要紧了。IndentationError: unexpected indent这一行比上一行缩进更深,而上面并没有任何东西开启了一个能解释这层缩进的代码块。通常是你粘进来的代码自带了前导空格,或者你删掉了 if 那一行,把它的主体留在了缩进里。把报错那一行开头的空白去掉即可;看不出差别时,用 cat -A 打印那一行,空格和制表符就现形了。TabError: inconsistent use of tabs and spaces in indentation同一个代码块里混用了制表符和空格,Python 无法判定哪一行更深。这多半是从会插入制表符的编辑器里粘贴,或者两个人用不同设置改同一个文件;屏幕上两种缩进看起来一模一样,靠眼睛找不出来。python -m tabnanny app.py 会打印出错的行号,剩下的就是在编辑器里把整个文件统一成四个空格。SyntaxError: invalid syntax解析器在那个位置无法把内容读成一条语句,而原因往往在它指出的那一行之前,而不是那一行本身。没闭合的括号或引号、漏掉的冒号、Python 2 的 print "x" 是常见原因——括号没闭合时,解析器会往后走好几行才放弃。python -m py_compile app.py 可以不运行只查语法;3.10 之后的提示具体得多,所以升级 Python 本身就是一种诊断。TypeError: 'NoneType' object is not subscriptable你用 [...] 取值的那个东西是 None。上游有什么悄悄返回了 None:缺键的 dict.get()、没匹配上的 re.match,或者在实际走过的分支上没有 return 的函数。不要在崩溃点加判断,往上游找它为什么是 None——一个跳过 None 的 if 只是把同一个错误挪到下一行。AttributeError: 'NoneType' object has no attribute 'get'点号左边那个对象是 None,所以你要的属性或方法不存在。它和 nonetype-not-subscriptable 同源,尤其常出现在直接接着一个失败时返回 None 的函数写下去的时候,比如 BeautifulSoup 的 find() 或 re.search()。先把那个函数在找什么打印出来;如果「找不到」本身是合法情形,就把这一支单独处理。KeyError: 'name'这个键不在 dict 里,引号中的字符串正是它去找的键——先从这里查,通常是拼写错误、大小写不同,或者 API 返回的结构和你以为的不一样。如果这个值确实可有可无,d.get("name") 会返回 None,d.get("name", 0) 还能给默认值;但对必须存在的值也用 .get,就是用一个 None 换掉了报错,它会流进代码深处,在很远的地方才炸。IndexError: list index out of range你要的位置列表里没有。长度为 n 时最大下标是 n-1,所以 range(len(x) + 1) 和 x[len(x)] 必然停在这里;对空列表取 x[0] 也是同一个错误——上面某个过滤或 split 什么都没留下时最常见。改成 for item in items: 遍历,这一类 bug 会从结构上消失;真的需要位置时用 enumerate(items)。ValueError: invalid literal for int() with base 10: '3.5'你给 int() 的是一个它无法读成整数的字符串,引号里的内容就是那个字符串。像 '3.5' 这样带小数点、空字符串、末尾带换行的输入,或 '1,000' 这样带千位分隔符,都是常见原因。带小数点的可以用 int(float(s)) 两步转换,但它是截断小数而不是四舍五入;只要是人手输入的值,用 try/except ValueError 包起来才是诚实的做法。UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte你把一个文件或字节串按 UTF-8 读,却碰到了 UTF-8 不允许的字节;消息里同时给出了位置和那个字节。多半是文件其实是 Windows 那边的 cp1252 或别的旧编码,或者根本不是文本——是图片、zip,或者忘了解压的 gzip 数据。正确做法是查出真实编码并用 encoding= 传进去;errors="replace" 能让读取走完,但会把那些字节换成问号,数据从此就悄悄坏掉了。ZeroDivisionError: division by zero有东西被零除了——实际上除数很少是字面的 0,而是数出来等于 0 的计数。算平均值时列表恰好为空,或者过滤条件一个都没匹配上,是最经典的来源:它在开发数据上永远通过,到线上才第一次炸。除之前先看分母是不是 0,并决定 0 在这里意味着什么;返回 0、返回 None、让异常继续往上抛,是三个不同的决定,各有各适用的场合。RecursionError: maximum recursion depth exceeded函数自我调用的深度超过了默认的 1000 层。相比真的需要那么深的计算,更多情况是终止条件缺失或永远到不了;间接递归也算,比如在 __getattr__ 或 property 里又去读自己的属性。先在 traceback 里找出反复出现的那两三个帧,把基线条件修好。sys.setrecursionlimit() 确实能抬高上限,但它同时让你越过真正的 C 栈,那时 Python 会直接死掉而不是抛异常——把递归改写成循环才是安全的答案。UnboundLocalError: cannot access local variable 'count' where it is not associated with a value只要函数体里任何一处给这个名字赋了值,Python 就把它当作函数的局部变量——而你在赋值执行之前就读了它。当你期望读到外层同名的值时,或者第一次写 count += 1 这类先读后写的运算时,就会碰到。通常的答案是在函数里先 count = 0;如果确实要改模块级的值,global count 能做到,代价是这个值由谁改写变得难以追踪。3.10 之前同一个错误写作 'local variable referenced before assignment'。TypeError: greet() takes 1 positional argument but 2 were given你传的参数比函数接受的多了一个;当数量正好差一个时,几乎总是 self。把类里的方法定义成 def greet(name):,调用 obj.greet("x") 时 Python 会把 obj 作为第一个参数传进去,于是就成了两个。如果它是方法,改成 def greet(self, name): 让它接收 self;如果它根本不需要实例,就加上 @staticmethod。PermissionError: [Errno 13] Permission denied操作系统拒绝了对那个路径的这个操作。你在往别人拥有的地方写,比如 /var/log 或 /usr;或者容器里挂载卷的 UID 和容器用户不一致;或者——最容易被漏掉的——你缺的是所在目录的写权限而不是文件的,而新建文件需要的正是目录权限。先用 ls -ld 看属主和权限位;在动用 sudo 之前,先考虑把路径换到你本来就能写的地方:用 sudo 建出来的文件,下次不带 sudo 打开时会再报同一个错。OSError: [Errno 98] Address already in usebind 失败是因为那个端口已经被另一个进程占着。可能上一个服务器没有真正退出,可能自动重载启了两份,也可能某个 Docker 容器已经发布了同一个端口。lsof -i :8000 会给出占用它的 PID,你只清掉那一个即可——在 kill -9 之前先看清进程名。errno 因系统而异:Linux 这里打印 98,macOS 打印 48。

构建与类型10

构建阶段的错误是工具在运行之前就先拒绝了,所以它们出现的那一刻并不弄坏什么——但它们问你两个问题:你要把类型或路径改成和真实结构一致,还是用一个 as any、一条抑制注释让那一行过去?也正是在这里,那些只在你自己机器上成立的事情——大小写、扩展名、环境变量——第一次显形。

error TS2307: Cannot find module 'lodash' or its corresponding type declarations.tsc 既没找到那个模块本身,也没找到它的类型声明,而这条消息同时说了两件事,这一点很关键——可能包压根没装,也可能装了但它不带类型。如果确实装了,那缺的就是类型:npm i -D @types/lodash 装上配套的类型包;不过如今很多包自带类型,根本没有对应的 @types。如果这个错出现在相对路径的 import 上,就去看大小写和 tsconfig 里的 paths——指向你自己文件的路径和 @types 毫无关系。error TS2339: Property 'user' does not exist on type 'Request'.那个类型里没有声明这个属性。它来自往库的类型上加一个它没有的字段(往 Express 的 Request 上挂 user 是典型)、拼写错误,或者在未收窄的联合类型上读只属于其中一支的属性。把类型改成和真实结构一致才是答案;是联合类型的话,用 'x' in y 或判别字段收窄,在那一支里访问就打开了。用 as any 压掉只是堵住编译器的嘴:那个位置从此连拼错的属性名也会放过。error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.函数接受的类型和你传进去的类型不一致,消息按顺序写明了两者——前面是你给的,后面是它要的。最常见的情形是表单字段、URL 查询串或 JSON 里的值以字符串形态进到了要数字的位置。在边界处转换一次是答案,但 Number(value) 失败时返回的是 NaN 而不是抛错,所以必须拦住这个 NaN 别往下流——Number.isFinite(n) 就是该放在这里的检查。error TS18048: 'user' is possibly 'undefined'.这个值可能是 undefined,而你没有处理这种可能;这是 tsc 替你提前抓住了一个将来的 Cannot read properties of undefined。它出现在可选字段、数组 find() 的结果,以及可能没设置的环境变量上。解决办法是决定「它不存在时怎么办」:在函数开头用 if (!user) return null 早退,或者用 user?.name 短路。写成 user!.name 压掉,等于声明「我保证它存在」——保证一错,它就以运行时异常回来,而且不留下检查本会留下的任何安全网。error TS7006: Parameter 'req' implicitly has an 'any' type.这个参数没写类型,tsc 也没有可推断的依据,于是它隐式变成了 any——而 noImplicitAny 把这当作错误。它出现在从 JavaScript 迁过来的文件里,以及把回调单独提到变量里的时候;相反,像 items.map(x => ...) 这样内联写的回调是有上下文的,tsc 能推断出来,就不会报这个。在那里把类型写上就是解法。你也可以显式写 any,但那是决定把这个参数的检查整个关掉——当你不知道它是什么时,unknown 更诚实,并且强迫你在使用前先收窄。Parsing error: Unexpected tokenESLint 是在读语法的阶段就停下的,还没来得及把任何规则套到这个文件上。相比代码真的坏了,更多情况是解析器不认识这套语法——用默认解析器读 TypeScript 文件、从未开启 JSX、把装饰器或很新的语法交给了旧解析器。对准那个文件运行 npx eslint --print-config app.ts,会直接打印真正生效的 parser 和 parserOptions,所以先看它,别猜。TypeScript 需要 @typescript-eslint/parser;如果这个文件本就不该被检查,把它写进 ignores 才是正确答案。You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.这是 webpack 在 Module parse failed 之后补上的那一行,意思是它想把那个文件当 JavaScript 读,而它不是 JavaScript。要么你 import 了需要转换的东西——TypeScript、JSX、CSS、图片、.vue 文件——而 module.rules 里没有对应规则;要么 node_modules 里某个包发布的是未编译的源码,而这个目录正躺在你的 exclude 里。在 module.rules 里为那个扩展名加一条 loader 规则就是解法,而卡在哪个文件,消息上方打印的路径会告诉你。为了让它消失而删掉 exclude: /node_modules/,会让整个构建明显变慢;只为那一个包开例外才是更好的交易。Failed to resolve import "./utils" from "src/main.ts". Does the file exist?那个路径下什么也没找到,而它一起打印的 "Does the file exist?" 指向真正值得查的三件事:大小写、扩展名,以及 tsconfig 或 vite.config 里的别名。macOS 和 Windows 的文件系统不分大小写,所以写 ./Utils 在你机器上照样跑,直到在 Linux 的 CI 和部署上才第一次崩——这个错误是「我这儿明明是好的」最常见的真身。git ls-files src | grep -i utils 能看出仓库里实际存的是哪种拼写。若用的是 @/utils 这类别名,vite.config 的 resolve.alias 和 tsconfig 的 paths 必须一致;只改一处,编辑器安静了,构建照旧失败。You're importing a component that needs useState. This React hook only works in a client component.在 App Router 里每个组件默认都是服务器组件,而某个服务器组件 import 了一个使用 useState 这类只在浏览器里才有意义的 hook 的文件。指令就是文件最顶端的一行 'use client',加上它会把这个文件以及它 import 的一切都拉进客户端包——所以只给真正需要状态的最小那块加,而不是整页加,才是便宜的选择。反方向上,把结构拆成「客户端组件把服务器组件当作 children 接收」,就能把取数据的活留在服务器。Next.js 13 里同样的情形写作 "It only works in a Client Component but none of its parents are marked with \"use client\""。The engine "node" is incompatible with this module. Expected version ">=20"你要装的包在 package.json 的 engines 字段里声明了它需要的 node 版本,而你当前用的版本落在这个范围之外。后面跟着的 Expected 和 Got 把要求和你的版本并排列出,看这两行就够了——如果只有 CI 上报这个,那就是它的 node 版本和你机器上的不一样。用 nvm install 20 && nvm use 20 升上去是正面的答案,并把同一个版本写进 .nvmrc 和 CI 配置,下次才不会再岔开。yarn 的 --ignore-engines 能硬闯过去,但它只是抹掉警告,并不制造兼容性:那个包一旦用了更新的语法,就改成在运行时以语法错误炸掉。npm 把同样的情形报成 EBADENGINE 警告,默认并不阻止安装。

Docker12

docker 的错误要先归到某一层才读得懂——客户端连不上守护进程、镜像仓库拒绝你、构建过程中某条 RUN 失败、容器一起来就死掉,是四种不同的问题——而在构建和运行这两层,那行字本身并不是原因:原因在里面那条命令留下的输出里,各层修复的代价也不一样。

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?docker 命令自己什么都不做,它通过那个 socket 请求守护进程,而没有人应答。要么守护进程没在运行,要么 macOS/Windows 上的 Docker Desktop 还没启动完,要么在 Linux 上你的用户不在 docker 组里、无权打开这个 socket;这三种情况都只打印这一行。Linux 上用 sudo systemctl start docker 启动;用 Desktop 就先把应用打开。如果是权限问题,sudo usermod -aG docker $USER 再重新登录即可——但要知道这个组实际上等同于 root。Bind for 0.0.0.0:8080 failed: port is already allocated你用 -p 要占宿主机的那个端口,而它已经被另一个容器或进程占着。最常见的是之前不加 --rm 启动的容器还留在那里(停着或跑着),或者 compose 还攥着一个旧容器。docker ps --filter publish=8080 会直接指出发布该端口的容器;如果其实是宿主机上的普通程序而不是容器,用 lsof -i :8080 找。只改宿主机一侧,比如 -p 8081:80,能让你先跑起来,但旧容器不清掉,下次还是同一场撞车。Conflict. The container name "/api" is already in use by container你用 --name 给的名字已经被另一个容器占了。名字在停止的容器和运行的容器之间是共享唯一的,所以多半是昨天挂掉的那个容器还攥着它——docker ps 里看不到,只有 docker ps -a 能看到。docker rm -f api 会释放这个名字,同时也会把那个容器的可写层一起销毁(具名卷会保留)。一次性用的容器,一开始就用 docker run --rm 启动,这种情况根本不会发生。failed to register layer: Error processing tar file(exit status 1): no space left on device磁盘上没有空间可以展开镜像层了。通常并不是项目大,而是 docker 还攥着几个月的旧镜像、构建缓存,以及你已删除容器留下的卷。docker system df 会先告诉你镜像、容器、卷和构建缓存各占多少、其中多少可回收,删任何东西之前先读它。docker system prune 会删掉停止的容器、悬空镜像、未使用的网络和构建缓存——缓存一没,下一次构建会明显变慢;再加 --volumes 还会删掉没挂在容器上的卷,人们丢数据库正是丢在这里。pull access denied for myapp, repository does not exist or may require 'docker login'镜像仓库不肯把那个仓库给你看,而关键在于这条消息同时涵盖两种原因:名字不存在,或者存在但你无权看到。要么你没有为私有仓库登录,要么名字缺了用户或组织(myapp 和 myorg/myapp 是两个不同的仓库),要么名字本身就写错了。私有镜像就对那个仓库 docker login;如果它本该是公开的,就再核一遍拼写。镜像仓库故意不区分「不存在」和「不给看」——因为一个名字是否存在本身就是信息。manifest for myapp:v2 not found: manifest unknown仓库找到了,但那个标签不在里面。和 pull access denied 不同,这条消息说明名字是对的,于是只剩标签要查——被删掉或挪走的标签、只推了 latest 而从未创建 v2 的流水线,或者那个标签下没有你这个架构的镜像。docker manifest inspect myapp:v2 能确认标签到底能否解析、里面有哪些 platform。为了先跑起来而改用 latest,代价是一个无法复现的构建,不如把标签改对。unauthorized: incorrect username or password镜像仓库拒绝了你发过去的凭据。比打错密码更常见的,是把密码发给了一个早已不接受账号密码的仓库——Docker Hub 开启双因素后只收访问令牌,而 GitHub、GitLab 的仓库从一开始就只要令牌或部署密钥。docker logout 清掉存下的凭据,然后用令牌重新 docker login。写成 echo $TOKEN | docker login -u user --password-stdin,令牌就不会留在 shell 历史里。failed to solve: process "/bin/sh -c npm ci" did not complete successfully: exit code: 1Dockerfile 里那条 RUN 以非零状态退出了。这一行携带的只有「哪条命令失败、退出码是几」,真正的原因在它上面那条命令自己的输出里,而 BuildKit 一旦某步结束就倾向于把它折叠起来。用 docker build --progress=plain 重跑,会把每一步的输出完整打印出来;如果是某个缓存层藏住了更早的失败,再加 --no-cache——代价是从第一步开始整个重建。exit code: 1 不是原因,只是「这条命令失败了」这个事实。COPY failed: file not found in build context or excluded by .dockerignoreCOPY 只能从构建上下文里取文件,而那个文件不在其中。上下文是 docker build 的最后一个参数,所以通常是你用 ../x 指向了上一层目录,或者 .dockerignore 把那个文件滤掉了——忽略了 node_modules 或 *.env,却又要 COPY 其中的文件,是最典型的。这条消息同时点出两种原因,所以先 cat .dockerignore,再按上下文根目录重写路径。靠扩大上下文来解决,意味着整个目录都要传给守护进程,每次构建都会变慢。exec /usr/local/bin/entrypoint.sh: exec format error内核认不出那个可执行文件的头部,而如今这几乎总是架构不匹配——在 Apple Silicon 上构建的 arm64 镜像跑在 amd64 主机上,或者反过来。shell 脚本少了第一行 #!/bin/sh 时也会打出同样的字。用 docker build --platform=linux/amd64 构建是常见做法,但它不是消除不匹配,而是用模拟把它盖住:经过 QEMU 的运行比原生慢好几倍。要长期使用的镜像,用 buildx 同时构建两种架构,才是运行时不付代价的路。standard_init_linux.go: exec user process caused: no such file or directory容器要执行它的入口点,内核回答「no such file」——陷阱在于那个文件明明就在那里。脚本的行尾是 CRLF,于是第一行被读成 #!/bin/sh\r,内核便去找一个名字真的叫 sh\r 的解释器;在 Windows 上克隆、或者开着 git 的 autocrlf,正是这个结果。dos2unix entrypoint.sh 能转换那一个文件(没有它就用 sed -i 's/\r$//' entrypoint.sh),而在 .gitattributes 里写一行 *.sh text eol=lf 能防止它再来。在 #! 里写了镜像中并不存在的解释器——比如 alpine 镜像里的 bash——也会给出同样的字。OCI runtime create failed: exec: "bash": executable file not found in $PATH: unknown容器建出来了,但你让它运行的程序在容器内部的 $PATH 里找不到。基于 alpine 的镜像只带 sh 而没有 bash,所以 docker run -it myapp bash 或者 CMD ["bash", ...] 恰好就以这个错误收场——你以为镜像里有、其实只存在于宿主机上的任何工具也一样。docker run --rm -it myapp sh 能先给你一个 shell,看清这个镜像里到底有什么。如果确实需要 bash,在 Dockerfile 里 RUN apk add --no-cache bash 就能装上,代价是镜像变大。

怎么读报错

  • 从第一行往下读。越往下越是工具内部的事,起因通常写在最上面。
  • 有文件名和行号就从那里查——不是栈顶那一帧,而是最上面那条提到你自己写的文件的行。
  • 把报错原文照样去搜,但先去掉你自己的路径和变量名,正是那些让搜索匹配不上。
  • 同一种情况在不同版本里措辞不同。结果不对头,就把版本号一起加进查询。
  • 粘贴修法之前,先确认它会丢掉什么。这里面有些是不能撤回的。

常见问题

Q. 为什么报错原文不翻译?

因为你要拿它去搜。工具输出的是英文,翻译过的原文什么也搜不到。跟着语言变的只有含义和处理办法。

Q. 我屏幕上的措辞略有不同。

工具在不同版本里会改措辞。去掉你自己的路径和名字后骨架一致,就是同一个报错。版本不同就把版本号一起加进搜索。

Q. 修法可以直接跑吗?

先看它会丢什么。git reset --hard、强制推送和 docker system prune 删的东西回不来——凡是这类,条目里都写了。

Q. 这里没有的报错怎么查?

把报错里你自己的路径、名字和数字去掉,只搜剩下的部分。那部分是工具作者写的句子,用它搜才对得上。