Asynchrony allows suspending a task while the runtime continues handling other operations. In DeviceScript, this is mainly expressed with async, await, and functions that return promises.
When you write delay(1000) in Arduino, the microcontroller “freezes.” It stays on that line counting milliseconds. If you press a button during that second, the Arduino doesn’t notice. If it was supposed to update a display, it doesn’t.
To solve this in C++, we had to resort to the famous “Blink Without Delay” and struggle with the millis() function, subtracting times and creating state machines. It works, but the code becomes messy and hard to read quickly.
In DeviceScript, we can pause a function without stopping the event loop, allowing the microcontroller to move on to other tasks. This is cooperative concurrency, not simultaneous execution on multiple cores.
The problem of blocking code
Think of a chef.
- Blocking model: You put bread in the toaster and stand in front of it doing nothing until it finishes.
- Asynchronous model: You put bread in the toaster and use the waiting time to do other tasks. When it finishes, you come back to pick it up.
DeviceScript uses this second model thanks to the Event Loop.
The async and await keywords
To manage this flow, TypeScript gives us two fundamental keywords: async and await.
async
When we define a function with async, we are telling the system: “Hey, this function might take a while to complete and may have pauses inside. Don’t block waiting for it.”
await
This is the key. await is used inside async functions. It means: “Pause the execution OF THIS FUNCTION here until the task finishes, but in the meantime, let the processor do other things.”
Waiting with delay
In DeviceScript, the delay function is imported from @devicescript/core. Unlike Arduino’s, this delay returns a Promise that resolves after the specified time.
See the difference:
import { delay } from "@devicescript/core"
import { startLightBulb } from "@devicescript/servers"
import { gpio } from "@devicescript/core"
const led = startLightBulb({ pin: gpio(2) })
// Define an asynchronous function
async function blink() {
while(true) {
await led.write(1)
// Pause here for 1 sec, BUT the processor remains free
await delay(1000)
await led.write(0)
await delay(1000)
}
}
// Start the task
blink()When the code reaches await delay(1000), the blink function “goes to sleep.” But the DeviceScript system stays awake: it can listen to buttons, receive WiFi data, or execute other functions.
Coordinating multiple tasks
Let’s make two LEDs blink at different rates without mixing their timers into a single loop.
In C++, we would need two unsigned long previousMillis variables, two intervals, and a lot of logic in the loop().
In DeviceScript, we simply launch two asynchronous functions:
import { delay } from "@devicescript/core"
import { startLightBulb } from "@devicescript/servers"
import { pins } from "@dsboard/esp32_c3_devkit_m" // Adjust for your board
// Define two virtual or real lights
const fastLed = startLightBulb({ pin: pins.GPIO2, roleName: "led1" })
const slowLed = startLightBulb({ pin: pins.GPIO3, roleName: "led2" })
// Task 1: Blinks every 200ms
async function fastTask() {
while (true) {
await fastLed.intensity.write(1)
await delay(200)
await fastLed.intensity.write(0)
await delay(200)
}
}
// Task 2: Blinks every 1000ms
async function slowTask() {
while (true) {
await slowLed.intensity.write(1)
await delay(1000) // 1 second
await slowLed.intensity.write(0)
await delay(1000)
}
}
// Launch both "at the same time"
fastTask()
slowTask()
console.log("Both tasks are running concurrently")In the simulator, two bulbs will appear with different rates. Each task preserves its own sequence, even though both share the same runtime.
We didn’t need to subtract timestamps. We described each behavior linearly, and the runtime interleaves the tasks through cooperative concurrency.
Common mistakes
The most common mistake when starting out is forgetting await.
// ❌ INCORRECT
async function badExample() {
console.log("Start")
delay(5000) // Missing await!
console.log("End")
}When running the example, Start and End appear without waiting five seconds.
By omitting await, we start the timer but don’t suspend the current function. Therefore, the code immediately continues to the next line.
If a function returns a promise (like delay or many read and write operations), use await when you need its result before continuing.