Callback tree resolve order question

I have an app with a bunch of input fields. After any of the fields changes, an sequence of error check callbacks takes place which sometimes uses inputs from other fields. The combined number of errors for all fields ‘num_error’ is stored in a Store. Including latency, compute time etc, it can take a small amount of time before this update is finished. Let’s say 1 second for the sake of argument.

I also have a button called ‘compute_button’ which should do the actual calculations, but only if ‘num_error’ is zero.

The dcc.Input fields in the form use debounce=True, so that this error check function is not triggered after each keystroke, but only when the field loses focus.

Now, an edge case can happen and I am not sure about how to catch it. The fields are all filled out and num_error = 0. Imagine the user then starts entering an erroneous value in one of the fields, and causes it to defocus by clicking on the ‘compute_button’. I think this sets of two chains of events (in parallel?).

  1. n_click of compute_button gets incremented by one
  2. The input field with the bad value loses focus and the error check function starts. This will eventually discover the problem and change num_error to 1
@app.callback(Output("someoutput", "data"),                 #A Store which triggers the real computation function if changed
              Input('compute_button', 'n_clicks'),          #Button
              Input("num_error", "data"), ...)             #Number of errors in the inputs as recorded by Store 
def computation(compute_button_n_clicks, num_error, ...):
    #Do stuff if the trigger was compute_button_n_clicks
    #But only if 'num_error = 0' 

I had hoped that Dash would realize that it should wait executing function computation() until its second input num_error() got updated. But that is not what happens. The function triggered by the button click will still think num_error = 0. The function incorrectly thinks there are no errors and will continue with the execution of the function. Sometime later the function will be triggered a second time, when num_error finally got updated from 0 to 1.

I guess this means that the user click indeed sets of two parallel callbacks as listed above, both unaware of each other? Is there a way I can ask the Dash API if there are ongoing parallel callbacks? In that case I could perhaps use logic to catch the situation described above.

(I guess I could make one mega-function which takes all inputs, performs all the error checks and eventually performs the computation if all is well. But this would make the code a lot harder to maintain and a lot messier. I would really like to avoid that situation)

EDIT: I did further testing on my side and I am more confused than before. I wrote an example which actually runs

import dash
import dash_core_components as dcc
import dash_html_components as html
import time
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Div("Simplified error check. If the user enters a positive number it is correct. Otherwise error."),
        html.Br(),
        dcc.Input( id="input_field",    type="number", placeholder="", debounce=True),
        html.Button('Compute', id='compute_button'),
        html.Div("", id="output"),
        dcc.Store(id='num_error')
    ]
)

@app.callback(Output("output", "children"),                 #Computation results
              Input('compute_button', 'n_clicks'),          #Button
              Input("num_error", "data"),                   #Number of errors in the inputs as recorded by Store 
              State('input_field',  'value'),
              )                       
def computation(compute_button_n_clicks, num_error, in_value):
    #Do stuff if the trigger was compute_button_n_clicks
    #But only if 'num_error = 0' 
    print("Entering computation()")
    
    if compute_button_n_clicks is None or num_error is None: #catch init callback (or empty input field)
        raise PreventUpdate() 
    
    ctx     = dash.callback_context
    print(ctx.inputs, ctx.states)
    
    if ctx.triggered[0]['prop_id'] == "num_error.data":     #num_error changed, but was no button click, don't need to do anything
        raise PreventUpdate() 
    
    return "Doing the computation with in_value=%s while thinking there are %i errors."%(str(in_value), num_error)
    
@app.callback(Output("num_error",   "data"),                #Store number of errors
              Input('input_field',  'value')                #Input field
              )             
def check_error(in_value):
    if in_value is None: #init callback for instance
        raise PreventUpdate()         
    
    error_count = 1 #initialize assuming error

    if in_value>0 :
        error_count = 0
    
    tsleep = 3  
    print("Inside check_error, working hard for %.2f seconds..."%tsleep)
    time.sleep(tsleep) #simulate hard work in a complicated error check function
    
    print("Finished error check and found %i errors"%error_count)
    return error_count
        
    
if __name__ == '__main__':
    app.run_server(debug=True, dev_tools_hot_reload=False)

First enter the number 1 in the input field and click somewhere else (triggering the error check function). Because the number 1 is the correct answer in this test, num_error = 0. Now enter the number 0 (representing a bad answer) and make the input field lose focus by clicking on the ‘Compute’ button. As wished for, it patiently waits for the updated error check and doesn’t trigger computation() by n_clicks in the meantime. As a result,

Confusion 1: If for the Compute() function I replace the Input(“num_error”, “data”) with State(“num_error”, “data”), then the error happens that I described above. (the ‘bad’ input value 0 enters computation() while it also thinks there are 0 errors). Something about Input makes it more eager to wait for the latest updated results than a State
enter image description here

Confusion 2: In my real code, I use an Input and not a State for “num_error” so I would expect things to work as in my standalone test code. But for some reason it doesn’t. The program is a lot bigger with many pattern matching callbacks and the error check functionality is broken into many components, each checking different parts, eventually summing the component results in other callbacks. But at the end, it still updates a single “num_error” so I don’t see why things would work differently because of that. The Dash debug functionality which normally shows how callbacks are related to each other with boxes and arrows doesn’t work anymore for my real app, perhaps related to its complexity.

I have become to suspect the problem is related to my full-sized code and how some components interact with each other. I requested the admin to delete this topic to avoid future confusion