A node can depend on its children being ready. Autoloads are also ready, since they're inserted at the base of the scene tree.
For other nodes, as you've observed, it depends on their position in the scene tree.
One common practice is "call down, signal up". That means that directly accessing properties or methods of a node should only be done by an ancestor of that node. Use signals to communicate with nodes that are neither descendants nor autoloads.
One approach I've used when I want node A to reference node B, and A is not an ancestor of B, is to handle it in a common ancestor of A and B. For example:
Node C is a common ancestor of A and B.
Node A has a property node_b:
var node_b: Node
Node C initializes the property node_b:
func _ready() -> void:
$NodeA.node_b = $NodeB
Another tactic is to use call_deferred(), since that doesn't execute until the scene tree has been fully set up and all the nodes are ready.