With your try with "$(cat)" you're almost there, but you need this cat to read from ifne, not along ifne.
In case of ifne notify-send "Error" "$(cat)", cat reads from the same stream ifne does, but not simultaneously. The shell handling this part of code can run ifne only after cat exits (because only then it knows what $(cat) should expand to, i.e. what arguments fine should get). After cat exits, the stream is already depleted and ifne sees its input as empty.
This is a way to make a similarly used cat read from ifne:
foo 2> >(ifne sh -c 'exec notify-send "Error" "$(cat)"')
(I'm not sure what the purpose of your 1> >(cat) was. I skipped it.)
Here ifne relays its input to the stdin of whatever it (conditionally) runs. It's sh, but everything this sh runs shares its stdin. Effectively cat reads from ifne. And similarly to your try, exec notify-send can be executed only after cat exits; so even if notify-send tried to read from its stdin, cat would consume everything first.
This method may fail if there is too much data passing through cat. Argument list cannot be arbitrarily long. And because cat will exit only after foo exits, the method works for foo that ever exits and generates not too many messages to its stderr.
Using xargs instead of $(cat) may be a good idea for long-running foo that occasionally generates a line of error. This is an example of such foo:
foo() {
echo a
echo b >&2
sleep 10
echo c
echo d >&2
sleep 20
}
The above solution is not necessarily good in case of this foo (try it). With xargs it's different. foo may even run indefinitely and you will be notified of errors (one line at a time) immediately. If your xargs supports --no-run-if-empty (-r) then you don't need ifne. This is an example command with xargs:
foo 2> >(xargs -r -I{} notify-send "Error" {})
(Note this xargs still interprets quotes and backslashes.)