In one of my scripts container_box.gd, in the _ready function I'm calling this

app_manager = get_node("path_to_app_manager_here")
app_state = app_manager.state
app_state.some_signal.connect(some_function)

In app_manager.gd I have the following

var state: AppState
func _ready():
    state = AppState.new()

With this setup I was getting app_state as null in container_box.gd. I noticed that this was happening because the AppManager node was defined after the ContainerBox in the tree. So moving the AppManager above the ContainerBox in the tree fixed the issue.
Another solution(which I went ahead with) to this was doing this directly instead of the ready function of AppManager

var state: AppState = AppState.new()

But this might not work for every scenario. What's the recommened practice for getting nodes/data in the ready function to ensure they are not null?

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.

You can also use 'await get_tree().root.ready' (I may have misnamed something. on my phone. but it's basically that)