Skip to main content

defer

The defer function registers a call to run when the enclosing script or steps.parallel task finishes, whether it returned normally, stopped with fail(), or was cancelled. Use it to tear down what a script started, such as an emulator, a temporary stack, or a lock, without a finally block the language does not have.

Usage​

defer(fn, *args, **kwargs)

Arguments​

fn
Required. The function to call later. Any callable works, including a lambda or a loaded function.
*args, **kwargs

(Optional) Arguments passed to fn when it runs. They are captured at the time of the defer call, as in Go, so a loop variable keeps the value it had when the call was registered.

Returns​

The return value is None.

Behavior​

  • Last in, first out. Call defer as many times as you like. Deferred calls run in reverse order of registration, so teardown mirrors setup.
  • Scoped to the script or task. A call registered at the top level or inside any function runs when the script body finishes. A call registered inside a steps.task function runs when that task attempt finishes, before the next attempt or task, with the task's output prefix. Each retry attempt unwinds its own deferred calls.
  • Runs after fail(). The original error is still the script's error. A failure inside a deferred call is reported after it and never masks it. When the body succeeded, a failing deferred call fails the script. The remaining deferred calls still run.
  • Runs after cancellation. The first Ctrl-C (SIGINT) cancels the script's own context, runs the deferred calls on a fresh context with a 30 second grace period, so exec.run and atmos.* calls inside them still work, and then exits with status 130. The exit is silent: Atmos prints no error box, like a shell, unless a deferred call fails, in which case that failure is reported and the status is still 130. A second Ctrl-C exits immediately without waiting for the remaining deferred calls. This holds for every embedded script session (standalone scripts, script steps, custom commands, workflows, and hooks). It is the automation-language form of a shell trap ... EXIT; scripts never install signal handlers themselves.
  • Nested registration. A deferred call may itself call defer; the new call runs after it.

Examples​

Tear down an emulator and a stack in reverse order​

atmos.emulator("up", "aws", flags = {"stack": "advanced"})
defer(lambda: atmos.emulator("down", "aws", flags = {"stack": "advanced"}, check = False))

atmos.aws("cfn", "deploy", "artifacts", flags = {"stack": "advanced"})
defer(lambda: atmos.aws("cfn", "delete", "artifacts", flags = {"stack": "advanced", "auto-approve": True}, check = False))

atmos.aws("cfn", "deploy", "app", flags = {"stack": "advanced"})

If the app deploy fails, the artifacts stack is deleted first and the emulator stopped second, and the deploy error is what Atmos reports.

Capture arguments at registration time​

def remove(path):
exec.run(["rm", "-f", path])

for path in ["a.tmp", "b.tmp"]:
defer(remove, path)

Both files are removed; each deferred call keeps its own path.

Clean up per task​

def test(stack):
lock = exec.run(["./lock", stack], output = "capture").stdout.strip()
defer(exec.run, ["./unlock", lock], check = False)
atmos.terraform("plan", "vpc", stack)

steps.parallel(tasks = [steps.task(name = s, function = test, args = [s]) for s in ["dev", "staging"]])

Each task unlocks its own stack when it finishes, even if the plan fails.

Errors​

  • A missing argument, or a first argument that is not callable, fails with an argument error.
  • A deferred call that fails reports deferred <name> failed: <reason>.