You are using save correctly, but you are missing that nu's internal commands do not "print to stderr".
If you want to catch an internal command (e.g. ls) that might fail, use try and catch:
try { ls nonexistent.file } catch { echo "mishap" | save error.txt }
# or
try { ls nonexistent.file } catch { |e| echo $e.msg | save error.txt }
If you want to capture the output that an external command (e.g. cat) has sent to STDOUT or STDERR in a do block, you can use save just like you have tried:
do { cat nonexistent.file } | save out.txt --stderr err.txt
But with external commands you can more directly also use out>, err>, and out+err> to channel their raw streams:
cat nonexistent.file out> out.txt err> err.txt
Finally, you might also be interested in using complete which provides you with stdout, stderr, and exit_code as keys in a record (which you can then further process like with to text | save … or to json | save … or get stderr | save … etc.):
do { cat nonexistent.file } | complete
╭───────────┬──────────────────────────────────────────────────╮
│ stdout │ │
│ stderr │ cat: nonexistent.file: No such file or directory │
│ │ │
│ exit_code │ 1 │
╰───────────┴──────────────────────────────────────────────────╯