Skip to content

Coroutine Syntax

Juju Adams edited this page Feb 21, 2023 · 83 revisions

The following are valid command macros used to define coroutines. Please remember that the code that goes in between each coroutine command macro is in separate GML functions. These functions are executed in the same scope (the coroutine root struct) but otherwise they cannot share local variables ("var" variables) due to being in separate functions.

 

 

Basics

CO_BEGIN and CO_END

coroutineRootStruct = CO_BEGIN
    show_debug_message("This is an example coroutine.");
CO_END

CO_BEGIN and CO_END are required to bracket all coroutine code. Coroutine commands must be placed within these two commands to be valid (and you will likely experience fatal compile errors otherwise). CO_BEGIN returns a coroutine root struct for the created coroutine instance. If you'd like to read values from the coroutine or control its execution using methods then you will need to keep a reference to the coroutine root struct.

Creating a coroutine will automatically add it to a global list of coroutines to be executed every frame. Once the coroutine has completed it will be removed from global execution and will be available for memory collection. Of course, if you're maintaining a reference to the coroutine beyond its completion then it will not be garbage collected until the reference you hold has also been discarded.

coroutineRootStruct = CO_BEGIN
    show_debug_message("This is an example parent coroutine.");
    
    CO_BEGIN
        show_debug_message("This is an example child coroutine.");
    CO_END
CO_END

Coroutine definitions can be nested inside each other so that one coroutine parent can create additional coroutine children. Child coroutines created inside a parent coroutine will continue executing regardless of whether the parent coroutine was paused, cancelled, or otherwise interacted with. Each child coroutine exists in its own scope such that variables inside each child coroutine are unique to that coroutine. Parent and child coroutines do not share variables, nor do child coroutines share variables with each other.

Child coroutines, if any are created, do not block execution of the parent coroutine - if you'd like child coroutines to block their parent coroutine's execution, please use the RACE or SYNC commands (or create your own functionality using AWAIT).

 

THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("This will");
    THEN
    show_debug_message("be displayed");
    THEN
    show_debug_message("in the");
    THEN
    show_debug_message("same frame");
CO_END
coroutineRootStruct = CO_BEGIN
    REPEAT 5 THEN
        show_debug_message("Five messages!");
    END
CO_END

THEN by itself has no particular meaning and if used without context will simply append code blocks onto the end of the preceding code block. However, THEN is required syntax in multiple places and should be used as directed for those commands.

 

CO_PARAMS.<variable>

function ShowPopUpMessageTwice(_message)
{
    CO_PARAMS.message = _message;
    
    return CO_BEGIN
        show_message(message);
        DELAY 1000 THEN
        show_message(message);
    CO_END
}

CO_PARAMS allows you to set variables in a coroutine before defining it. This is very helpful for passing in arguments if your coroutine is located inside a function that starts new coroutines.

 

CO_ON_COMPLETE

coroutineRootStruct = CO_BEGIN
    list = ds_list_create();
CO_ON_COMPLETE
    //Clean up the list to avoid a memory leak
    ds_list_destroy(list);
CO_END

CO_ON_COMPLETE adds additional, final code to be executed by the coroutine when it completes. CO_ON_COMPLETE will also be executed when the .Restart() method is called.

Please note that the contents of CO_ON_COMPLETE code must be simple GML. This means you cannot use coroutine commands within the code block.

 

CO_SCOPE = <struct/instance>

This is an advanced feature provided for convenience and should not be used without due care and attention.

////Create Event of an object
//Set the scope of the next coroutine to ourselves (CO_SCOPE is reset by CO_END)
CO_SCOPE = self;
//Start 
CO_BEGIN
    WHILE true THEN //Repeat forever!
        //Randomize our position and angle
        image_angle = random(360);
        x = xprevious + random_range(-5, 5);
        y = yprevious + random_Range(-5, 5);
        
        //Wait 120ms before doing this again
        DELAY 120 THEN
    END
CO_END
////Draw Event
draw_self();

It is useful, from time to time, to have a coroutine interact directly with an instance's (or struct's) state directly. CO_PARAMS and .GetCreator() are provided to help smooth interaction between a coroutine and other data containers in your game, but calling code in a specific scope can be advantageous.

When a coroutine is generated, all code between each command is collected together in a native GameMaker function. The scope of this function, by default, is forced to be the root coroutine struct. This ensures instance variables are always created and modified in an isolated environment. Whilst this is substantially safer than alternatives, it can also be inconvenient. CO_SCOPE overrides the default behaviour (scoping to the coroutine struct) such that functions are scoped to an instance or struct of your choosing.

The coroutine struct is still generated, however, and will exist as an endpoint to call coroutine methods. All coroutine methods are still accessible by directly referencing the coroutine struct returned by CO_BEGIN.

CO_SCOPE applies to the next coroutine definition, and the next definition only. When CO_END is called, CO_SCOPE will be reset to the default behaviour (scoping to the root coroutine struct). In this regard, CO_SCOPE is similar to CO_PARAMS.

Please note that by using CO_SCOPE it is very easy to create race conditions where two coroutines fight to set the same value for an instance. This can lead to unpleasant and tricky bugs to fix. You use this feature at your own risk.

 

CO_LOCAL.<variable>

CO_PARAMS.cells_to_travel = 10;
CO_SCOPE = self;
CO_BEGIN
    CO_LOCAL.i = 0; //Use a coroutine variable to count how many times we've moved
    WHILE CO_LOCAL.i < CO_LOCAL.cells_to_travel THEN
	    
        //Move down the grid, 32px at a time
        y += 32; 
        
        //Change our sprite
        sprite_index = sprMoveDown;
        
        //Wait 90ms before doing this again
        DELAY 90 THEN
    END
CO_END

CO_LOCAL contains a reference to the coroutine that is currently being processed. By default, CO_LOCAL will be the self scope inside coroutine code blocks. This changes if you're using CO_SCOPE (see above) because the coroutine code blocks are now running in the scope of some other instance/struct. In order to be able to reference sandboxed variables held by the coroutine root struct, CO_LOCAL is required.

 

 

Returning Values

YIELD <expression> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("This will");
    YIELD THEN
    show_debug_message("be displayed");
    YIELD THEN
    show_debug_message("over several");
    YIELD THEN
    show_debug_message("different frames");
CO_END
coroutineRootStruct = CO_BEGIN
    i = 1;
    REPEAT 5 THEN
        YIELD i THEN //Yield the values 1, 2, 4, 8, 16 over 5 frames
        i *= 2
    END
CO_END

YIELD instructs the coroutine to temporarily stop execution of the coroutine and return a value. Unlike PAUSE...THEN or RETURN, execution will resume in the next frame without any other action required. The value emitted by YIELD can be read from the coroutine using the .Get() method. If no value is specified between YIELD and THEN then undefined will be returned by .Get().

Please note that a YIELD command must be followed by a THEN command. If you forget a THEN command then code will mysteriously fail to run and will appear to be "skipped over".

 

PAUSE <expression> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("Look left");
    PAUSE "left" THEN
    show_debug_message("Look right");
    PAUSE "right" THEN
    show_debug_message("Look left again");
    PAUSE "left" THEN
    show_debug_message("Then cross the road");
CO_END

PAUSE instructs the coroutine to immediately pause execution and return a value. This behaves similarly to YIELD but, unlike YIELD, a paused coroutine will not resume execution on the next frame. You will instead need to call the .Resume() method to resume execution of a paused coroutine. The value emitted by PAUSE can be read from the coroutine using the .Get() method. If no value is specified between PAUSE and THEN then undefined will be returned by .Get().

Please note that a PAUSE command must be followed by a THEN command. If you forget a THEN command then code will mysteriously fail to run and will appear to be "skipped over".

 

RETURN <expression>

coroutineRootStruct = CO_BEGIN
    IF oPlayer.x > 55 THEN
        RETURN "Too far right"
    ELSE
        CutsceneFindMyFroggy();
        RETURN "Playing cutscene"
    END_IF
CO_END

RETURN instructs the coroutine to immediately complete execution and return the given value. Unlike YIELD or PAUSE, the coroutine's execution is stopped entirely (though the coroutine may be restarted with the .Restart() method). The value emitted by RETURN can be read from the coroutine using the .Get() method. If no value is specified after RETURN then undefined will be returned by .Get().

Please note that a RETURN command does not need to be followed by a THEN command. Anything written after the RETURN command will, of course, never be executed, much like GML's native return command.

 

RESTART

coroutineRootStruct = CO_BEGIN
    CreateSmokeParticle(oChimney.x, oChimney.y);
    DELAY random_range(300, 350) THEN
    RESTART
CO_END

RESTART instructs the coroutine to yield and then, on the next coroutine frame, restart execution. A coroutine that has been restarted will return undefined if the .Get() method is called on the coroutine struct. By placing RESTART at the end of a coroutine you can get a coroutine to loop endlessly until otherwise cancelled. CO_ON_COMPLETE will be called when restarting a coroutine.

Please note that variables in the coroutine will not be reset.

Please note that a RESTART command does not need to be followed by a THEN command. Anything written after the RESTART command will never be executed, much like GML's native return command.

 

 

Loops

END

coroutineRootStruct = CO_BEGIN
    REPEAT 5 THEN
        show_debug_message("Five messages!");
    END
CO_END

Has no utility on its own. END is, however, necessary to terminate a REPEAT, WHILE, or FOREACH loop. It should also be used to terminate a RACE or SYNC block. It must not be used in other contexts.

 

REPEAT <expression> THEN <function> END

coroutineRootStruct = CO_BEGIN
    REPEAT 5 THEN
        show_debug_message("Five messages!");
    END
CO_END

Analogous to GameMaker's own repeat() loop. It is not necessary to use this macro in all cases to replace standard repeat() loops. The use of a REPEAT...END loop is required only when the repeat() loop would otherwise contain a coroutine command.

 

WHILE <condition> THEN <function> END

coroutineRootStruct = CO_BEGIN
    fireball = instance_create_depth(oPlayer.x, oPlayer.y, 0, oFireball);
    
    //Wait until the fireball has risen above the player by 30 pixels
    WHILE fireball.y <= fireball.ystart - 30 THEN
        fireball.y -= 5;
        YIELD THEN
    END
    
    //Then shoot the fireball at the nearest enemy!
    nearest = instance_nearest(fireball.x, fireball.ystart, oEnemy);
    fireball.direction = point_direction(fireball.x, fireball.y, nearest.x, nearest.y);
    fireball.speed = 11;
CO_END

WHILE is analogous to GameMaker's own while() loop. It is not necessary to use this macro in all cases to replace standard while() loops. The use of a WHILE...END loop is required only when the while() loop would otherwise contain a coroutine command.

 

FOREACH <iteratorVariable> IN <iterableData> THEN <function> END

coroutineRootStruct = CO_BEGIN
    highestHP = 0;
    highestInstance = noone;
    
    //Find the enemy from our array with the highest HP
    FOREACH instance IN global.arrayOfEnemies THEN
        if (instance.hp > highestHP)
        {
            highestHP = instance.hp;
            highestInstance = instance;
        }
    END
    
    //Bash them!
    if (instance_exists(lowestInstance)) hp -= 100;
CO_END

FOREACH...THEN loops are a convience feature that iterates over either

  1. an array,
  2. a struct,
  3. instances of an object,
  4. or the YIELD output from a coroutine.

When iterating over an array, the iterator variable is given values from the array itself. When iterating over a struct, the iterator variable is given values from the struct; to iterate over struct keys please use variable_struct_get_names().

When iterating over instances of an object, the iterator variable is given instance references (the struct representation of an instance that you get from e.g. calling self in the scope of an instance). Please note that FOREACH loops behave differently to GameMaker's native with() loops: the scope of code inside the FOREACH loop does not change.

When iterating over the output from a coroutine, the YIELD value is assigned to the iterator variable. The FOREACH...THEN loop will terminate when the iterable coroutine completes.

Please note that care should be taken not to modify an array or struct that you are iterating over. The total number of iterations is calculated when the FOREACH...THEN loop begins and if the size of the array or struct changes then this may cause crashes and other errors.

 

BREAK

coroutineRootStruct = CO_BEGIN
    healthRemaining = oPlayer.hp;
    FOREACH heart IN global.heartInstances THEN
        heart.sprite_index = min(4, healthRemaining);
        healthRemaining -= 4;
        if (healthRemaining <= 0) BREAK;
    END
CO_END

Analogous to GameMaker's own break command. Escapes either a REPEAT...THEN, WHILE...THEN, or FOREACH...THEN loop immediately without executing the rest of the code in the loop. The remainder of the code in the coroutine will execute as normal.

Please note that the standard GML break command will not work on coroutine loops.

 

CONTINUE

coroutineRootStruct = CO_BEGIN
    FOREACH enemy IN objEnemy THEN
        IF point_distance(oPlayer.x, oPlayer.y, enemy.x, enemy.y) > 100 THEN
            CONTINUE
        END_IF
        
        enemy.vspeed -= 4;
    END
CO_END

Analogous to GameMaker's own continue command. Forces a coroutine loop (either a REPEAT...THEN, WHILE...THEN, or FOREACH...THEN loop) to immediately proceed to the next iteration without executing the rest of the code in the loop.

Please note that the standard GML continue command will not work on coroutine loops.

 

 

Branching

IF <condition> THEN <function> END_IF (and ELSE and ELSE_IF)

coroutineRootStruct = CO_BEGIN
    healthRemaining = oPlayer.hp;
    
    FOREACH heart IN global.heartInstances THEN
        heart.sprite_index = min(4, healthRemaining);
        healthRemaining -= 4;
        
        IF (healthRemaining <= 0) THEN
            BREAK;
        END_IF
        
        YIELD THEN
    END
CO_END

Analogous to GameMaker's own if and else commands. An IF must be matched by an END_IF. It is typically not required to use these particular commands. You should use these macros if the if-branch (or else-branch etc.) contains a coroutine command itself, with the exception of ASYNC_COMPLETE. ELSE and ELSE_IF are also supported.

Please note that ELSE IF is incorrect syntax and will lead to compile errors, please ensure you use ELSE_IF.

 

 

Helpers

DELAY <expression> THEN

coroutineRootStruct = CO_BEGIN
    WHILE instance_exists(oRainbow) THEN
        oRainbow.image_blend = c_red;
        DELAY 500 THEN
        oRainbow.image_blend = c_orange;
        DELAY 500 THEN
        oRainbow.image_blend = c_yellow;
        DELAY 500 THEN
        oRainbow.image_blend = c_lime;
        DELAY 500 THEN
        oRainbow.image_blend = c_aqua;
        DELAY 500 THEN
        oRainbow.image_blend = c_purple;
        DELAY 500 THEN
    END
CO_END

DELAY is a convenience behaviour that will pause a coroutine for an amount of real world time. The duration of the delay is measured in milliseconds; a second is 1000 milliseconds, and at 60FPS a single frame is (approximately) 16.66ms.

Please note that whilst a coroutine is waiting at a DELAY command, the .GetPaused() method will not return true.

 

AWAIT <condition> THEN

coroutineRootStruct = CO_BEGIN
    fireball = instance_create_depth(oPlayer.x, oPlayer.y, 0, oFireball);
    fireball.hspeed = -5;
    
    //Wait until the fireball has risen above the player by 30 pixels
    AWAIT fireball.y <= fireball.ystart - 30 THEN
    
    //Then shoot the fireball at the nearest enemy!
    nearest = instance_nearest(fireball.x, fireball.ystart, oEnemy);
    fireball.direction = point_direction(fireball.x, fireball.y, nearest.x, nearest.y);
    fireball.speed = 11;
CO_END

AWAIT is a convenience behaviour that will check its condition before continuing with code execution. If the condition returns true then execution will continue immediately. However, if the condition returns false then the coroutine will temporarily stop execution until the next frame (much like YIELD...THEN, albeit AWAIT will yield with a value of undefined).

Please note that whilst a coroutine is waiting at an AWAIT command, the .GetPaused() method will not return true.

 

AWAIT_FIRST <coroutine> ... END

coroutineRootStruct = CO_BEGIN
    AWAIT_FIRST
        CO_BEGIN
            DELAY 200 THEN
            show_debug_message("First coroutine finished");
        CO_END
        
        CO_BEGIN
            DELAY 100 THEN
            show_debug_message("Second coroutine finished");
        CO_END
    END
    
    show_debug_message("Race finished (second coroutine should finish first)");
CO_END

AWAIT_FIRST allows for a parent coroutine to temporarily halt execution until one of the defined child coroutines has finished. Execution in the parent coroutine will continue once any of the child coroutines has completed execution; the remaining unfinished child coroutines will immediately be cancelled. Only coroutines defined inside the AWAIT_FIRST...END block will be considered for this behaviour and any previously created child coroutines will be disregarded for the purposes of AWAIT_FIRST logic.

Each child coroutine exists in its own scope such that variables inside each child coroutine are unique to that coroutine. Parent and child coroutines do not share variables, nor do child coroutines share variables with each other. All coroutines will execute CO_ON_COMPLETE functions regardless of whether that coroutine was the first one to end.

Please note Unlike normal child coroutines, pausing or cancelling the parent coroutine will pause or cancel child coroutines created inside a AWAIT_FIRST...END block.

Please note that whilst a coroutine is waiting at a AWAIT_FIRST command, the .GetPaused() method will not return true.

 

AWAIT_ALL <coroutine> ... END

coroutineRootStruct = CO_BEGIN
    AWAIT_ALL
        CO_BEGIN
            DELAY 200 THEN
            show_debug_message("First coroutine finished");
        CO_END
        
        CO_BEGIN
            DELAY 100 THEN
            show_debug_message("Second coroutine finished");
        CO_END
    END
    
    show_debug_message("Sync finished (both coroutines should have finished)");
CO_END

AWAIT_ALL allows for a parent coroutine to temporarily halt execution until all of the defined child coroutines have finished. Execution in the parent coroutine will continue once all of the child coroutines have completed execution. Only coroutines defined inside the AWAIT_ALL...END block will be considered and any previously created child coroutines will be disregarded for the purposes of AWAIT_ALL logic.

Each child coroutine exists in its own scope such that variables inside each child coroutine are unique to that coroutine. Parent and child coroutines do not share variables, nor do child coroutines share variables with each other.

Please note Unlike normal child coroutines, pausing or cancelling the parent coroutine will pause or cancel child coroutines created inside a AWAIT_ALL...END block.

Please note that whilst a coroutine is waiting at a AWAIT_ALL command, the .GetPaused() method will not return true.

 

AWAIT_BROADCAST <name> THEN

coroutineRootStruct = CO_BEGIN
    //Rotate the door 30 degrees so it's ajar
    WHILE image_angle < 30 THEN
        image_angle += 5
        YIELD
    END
    
    //Wait for the player to push right...
    AWAIT_BROADCAST "push right" THEN
    
    //...then open the door all the way!
    WHILE image_angle <= 90 THEN
        image_angle = min(90, image_angle + 5);
        YIELD
    END
CO_END

///Elsewhere in the player object...
if (keyboard_check(vk_right))
{
    CoroutineBroadcast("push right");
    hspeed = 2;
}

AWAIT_BROADCAST is a useful command that allows for indirect, if basic, control over coroutines. When a coroutine encounters an AWAIT_BROADCAST command, the coroutine will pause at that command. In order for the coroutine to proceed, CoroutineBroadcast() must be called using the same name as is given in AWAIT_BROADCAST command. The coroutine will then continue execution the next time CoroutineEventHook() is called in a Step event (usually on the next frame). If multiple coroutines are awaiting a broadcast with the same name, only one call to CoroutineBroadcast() is necessary to resume all those coroutines.

Please note that whilst a coroutine is waiting at an AWAIT_BROADCAST command, the .GetPaused() method will not return true.

AWAIT_BROADCAST will, by default, only respond to native Coroutines broadcasts. To listen for GameMaker broadcasts from sprites and sequences then please use AWAIT_ASYNC_BROADCAST.

 

 

Async Events

AWAIT_ASYNC_* <function> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("Starting leaderboard pull");
    handle = steam_download_scores("Game Scores", 1, 10);
    
    AWAIT_ASYNC_STEAM
        if (async_load < 0)
        {
            show_debug_message("Leaderboard request timed out");
        }
        else if (async_load[? "id"] == handle)
        {
           show_debug_message("Leaderboard data received");
            global.scores = array_resize(0);
            
            var _list = map[? "entries"];
            var _i = 0;
            repeat(ds_list_size(_list))
            {
                var _data = _list[| _i];
                array_push(global.scores, {name : _data[? "name"], score : _data[? "score"], rank : _data[? "rank"]});
                _i++;
            }
            
            ASYNC_COMPLETE
        }
    THEN
    
    show_debug_message("Leaderboard pull complete");
CO_END

AWAIT_ASYNC_* commands (for the full list, see below) allows a coroutine to interact with GameMaker's native async event system. When the coroutine encounters an AWAIT_ASYNC_* command, the coroutine will pause at that line of code and wait until the relevant async event is triggered. Once an async event of the correct type is surfaced by GameMaker's runtime, the async code in the AWAIT_ASYNC_* ... THEN block is executed. If the code block calls ASYNC_COMPLETE then the coroutine immediately executes further code, otherwise AWAIT_ASYNC_* continues to listen for new events.

The standard GML function located between AWAIT_ASYNC_* and THEN is executed every time that async event is triggered, regardless of whether that async event is relevant for the coroutine. This is unfortunate, but it's also the way GameMaker is designed. You should always check if the async_load or event_data ds_map you have received matches the async event you're expecting.

The code that follows the AWAIT_ASYNC_* command cannot contain any coroutine macros (with the exception of ASYNC_COMPLETE). This is because async_load and event_data may contain volatile data will not persistent beyond the end of the async event. If you'd like to perform extensive operations on data that's returned by an async event, you should make a copy of it and then process that data outside of the AWAIT_ASYNC_* code block.

AWAIT_ASYNC_* code may be executed when an operation has timed out. By default, no timeout duration is set and operations may hang forever. You can customize the timeout duration using the ASYNC_TIMEOUT macro (see below). When an async operation times out, async_load is a negative number. You should always write code to check if an async operation has timed out i.e. you should always handle the cases where async_load or event_data is negative.

Please note that AWAIT_ASYNC_BROADCAST will pick up specifically GameMaker sprite and sequence broadcasts; it will not pick up broadcasts native to the Coroutines library (use AWAIT_BROADCAST instead for that). Additionally, inside a AWAIT_ASYNC_BROADCAST code block you should be checking against event_data instead of async_load.

The following async await commands are supported:

  • AWAIT_ASYNC_HTTP
  • AWAIT_ASYNC_NETWORKING
  • AWAIT_ASYNC_SOCIAL
  • AWAIT_ASYNC_SAVE_LOAD
  • AWAIT_ASYNC_DIALOG
  • AWAIT_ASYNC_SYSTEM
  • AWAIT_ASYNC_STEAM
  • AWAIT_ASYNC_BROADCAST

These are the most common async events that get used in GameMaker. If you'd like additional async events to be added then please let me know and they'll go into an official release.

 

ASYNC_COMPLETE

coroutineRootStruct = CO_BEGIN
    handle = get_string_async("Please enter your name", "Juju Adams");
    result = "";    
    AWAIT_ASYNC_DIALOG
        if (async_load[? "id"] == handle)
        {
            if (async_load[? "status"]) result = async_load[? "string"];
            ASYNC_COMPLETE
        }
    THEN
    
    RETURN result;
CO_END

ASYNC_COMPLETE is an essential component of the AWAIT_ASYNC_* command. It indicates that the async operation has completed and the coroutine should continue execution of code. If you do not call ASYNC_COMPLETE inside your async code block then the async operation may hang indefinitely.

Please note that ASYNC_COMPLETE should not be called outside of AWAIT_ASYNC_* code blocks otherwise you will see unpredictable behaviour.

 

AWAIT_ASYNC_* <function> ASYNC_TIMEOUT <duration> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("HTTP GET started");
    handle = http_get("https://www.jujuadams.com/");
    AWAIT_ASYNC_HTTP
        if (async_load < 0) //Handle the timeout case
        {
            show_debug_message("HTTP GET timed out");
            ASYNC_COMPLETE
        }
        if (async_load[? "id"] == handle)
        {
            if (async_load[? "status"] == 0)
            {
                show_debug_message("HTTP GET succeeded");
                show_debug_message(async_load[? "result"]);
                ASYNC_COMPLETE
            }
            else if (async_load[? "status"] < 0)
            {
                show_debug_message("HTTP GET failed with error code " + string(async_load[? "http_status"]));
                ASYNC_COMPLETE
            }
        }
    ASYNC_TIMEOUT 6000 THEN //Wait 6 seconds before timing out (6000 milliseconds)
    show_debug_message("HTTP GET complete");
CO_END

Async operations, especially those to servers, often run into issues and requests time out. ASYNC_TIMEOUT...THEN adds a timeout behaviour to an AWAIT_ASYNC_* command to handle cases where unreported failure is possible. By default, AWAIT_ASYNC_* commands have no timeout duration and it is possible for operations to hang forever. The timeout duration (the number between ASYNC_TIMEOUT and THEN) is measured in milliseconds.

When async code is executed but the operation has timed out, async_load will be set to a negative number. You should always write behaviour into your AWAIT_ASYNC_* blocks to handle cases where async_load is a negative number to avoid unexpected problems.