A common misconception in Angular development is regarding whether observables are synchronous or asynchronous. A lot of (even experienced Angular developers) think that observables are async, but the truth is that they can be…
Both synchronous and asynchronous.
To understand why this is, let’s define what it actually means to run code async in the browser.
When is code running asynchronously in the browser?
Now, when any of these asynchronous browser API functions are being called, the execution is scheduled on a task queue (either macro or micro task queue). The a task queue is being emptied only when the stack is empty. This can be illustrated with these steps:
This is how the event loop works:
1) Run functions from the stack
2) Call async browser API function
3) Add them to the task queue (either micro or macro task queue – more on this soon)
4) Empty stack/execute all pending operations on the stack
5) Move a task from a task queue to the stack
6) Execute the task on the stack (stack becomes empty again)
7) Repeat step 5-6 until the task queue is empty
For a more in-depth description of how the event-loop works I recommend you to check out this video: https://www.youtube.com/watch?v=8aGhZQkoFbQ
Nevertheless, this is all we need to know for this blog post. Let’s not make things too complicated here.
Macrotasks and microtasks
I don’t intend to bore you with any more of the low-level browser details but understanding these concepts will help you distinguish the different kinds of async tasks, that can be run. The ones we just explored were the macro tasks. Other kinds of async tasks are micro tasks which happen when promises are used.
In the previous description of the event loop, the task queue we were talking about was the macro task queue, ie. task queue for callbacks from eg. DOM, XMLHttpRequest, and setTimeout.
Micro and macro tasks have separate queues and work almost the same way. The difference is that the micro task queue has a higher priority than the macro task queue. That means that when using promises in the event loop, the promise callbacks will execute before the macrotasks and the macrotasks will only be executed when the microtask queue is completely emptied.
Both macrotasks and microtasks are considered asynchronous, so using either of them in an Observable stream will per definition make the stream asynchronous.
The execution order can be visualized like this:
This answer wouldn’t be complete if I didn’t also touch on the Schedulers in RxJS. The schedulers in RxJS can determine if an observable is run synchronously, on the macro task queue or on the microtask queue.
Let’s see a demo of how the different schedulers can be used to determine the execution of observables
Also, note that the animation frame scheduler is used here, which will run when the browser repaints and is triggered when
requestAnimationFrame from the browser API is fired.
This gives the results:
This shows that queue is running synchronously as it is triggered before the other synchronous “after subscription” console log.
After this comes the asynchronous schedules observables. The asap scheduler runs next because it runs as a micro task, then the animation frame scheduler and then lastly the async because it runs as a macro task.
Observables: async or sync shouldn’t change the design
Now we know that observables in itself are synchronous (at least with the synchronous scheduler) as they are just registered callbacks to be executed on the stack. Remember stack only = synchronous.
What makes observables smart is the same reason this confusion came up in the first place: it is designed to be used the same way regardless if it is synchronous or asynchronous. That means you don’t need to worry about timing problems and handling state in a specific order as you just subscribe to the stream and that will give you the data when it is ready. The subscribe callback can be triggered right away if it is synchronous or delayed if it is asynchronous.
Cool story, how can I apply this knowledge?
If you are a regular reader of my blog, you know I don’t just drop buzzwords and “interesting” knowledge without it being valuable in specific practical scenarios.
You can apply this knowledge by knowing the execution order of observables. Especially when writing unit tests involving an observable, you would normally stub out all the dependencies making the observable execute synchronously. Here you can trust that a subscribe will be executed before the code underneath.
Note, that if you are doing asserts inside of a subscribe, you need to also call the done callback in the test. Otherwise, you could risk the subscribe callback never being called and the tests would still show as passed, giving you a false positive.
In this post, we saw that observables can by definition both be synchronous and asynchronous because it is determined whether the asynchronous browser API functions are being executed. We also looked at how the event loop works, what the difference is between macrotasks and microtasks and how to apply all of this in RxJS with schedulers.