Skip to main content

Errors and Debugging

This page contains practical examples for handlign and debugging errors in Dagger workflows.

Terminate gracefully

The following Dagger Function demonstrates how to handle errors in a workflow.

package main

import (
"context"
"errors"
"fmt"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Generate an error
func (m *MyModule) Test(ctx context.Context) (string, error) {
out, err := dag.
Container().
From("alpine").
// ERROR: cat: read error: Is a directory
WithExec([]string{"cat", "/"}).
Stdout(ctx)
var e *dagger.ExecError
if errors.As(err, &e) {
return fmt.Sprintf("Test pipeline failure: %s", e.Stderr), nil
} else if err != nil {
return "", err
}
return out, nil
}

Example

Execute a Dagger Function which creates a container and runs a command in it. If the command fails, the error is captured and the Dagger Function is gracefully terminated with a custom error message.

dagger -c test

Continue using a container after command execution fails

The following Dagger Function demonstrates how to continue using a container after a command executed within it fails. A common use case for this is to export a report that a test suite tool generates.

note

The caveat with this approach is that forcing a zero exit code on a failure caches the failure. This may not be desired depending on the use case.

package main

import (
"context"
"fmt"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

var script = `#!/bin/sh
echo "Test Suite"
echo "=========="
echo "Test 1: PASS" | tee -a report.txt
echo "Test 2: FAIL" | tee -a report.txt
echo "Test 3: PASS" | tee -a report.txt
exit 1
`

type TestResult struct {
Report *dagger.File
ExitCode int
}

// Handle errors
func (m *MyModule) Test(ctx context.Context) (*TestResult, error) {
ctr, err := dag.
Container().
From("alpine").
// add script with execution permission to simulate a testing tool
WithNewFile("/run-tests", script, dagger.ContainerWithNewFileOpts{Permissions: 0o750}).
// run-tests but allow any return code
WithExec([]string{"/run-tests"}, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny}).
// the result of `sync` is the container, which allows continued chaining
Sync(ctx)
if err != nil {
// unexpected error, could be network failure.
return nil, fmt.Errorf("run tests: %w", err)
}
// save report for inspection.
report := ctr.File("report.txt")

// use the saved exit code to determine if the tests passed.
exitCode, err := ctr.ExitCode(ctx)
if err != nil {
// exit code not found
return nil, fmt.Errorf("get exit code: %w", err)
}

// Return custom type
return &TestResult{
Report: report,
ExitCode: exitCode,
}, nil
}

Example

Continue executing a Dagger Function even after a command within it fails. The Dagger Function returns a custom TestResult object containing a test report and the exit code of the failed command.

Obtain the exit code:

dagger -c 'test | exit-code'

Obtain the report:

dagger -c 'test | report | contents'

Debug workflows with the interactive terminal

Dagger provides two features that can help greatly when trying to debug a workflow - opening an interactive terminal session at the failure point, or at explicit breakpoints throughout your workflow code. All context is available at the point of failure. Multiple terminals are supported in the same Dagger Function; they will open in sequence.

The following Dagger Function opens an interactive terminal session at different stages in a Dagger workflow to debug a container build.

package main

import (
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

func (m *MyModule) Container() *dagger.Container {
return dag.Container().
From("alpine:latest").
Terminal().
WithExec([]string{"sh", "-c", "echo hello world > /foo && cat /foo"}).
Terminal()
}

Example

Execute a Dagger Function to build a container, and open an interactive terminal at two different points in the build process. The interactive terminal enables you to inspect the container filesystem and environment "live", during the build process.

dagger -c container

Inspect directories and files

The following Dagger Function clones Dagger's GitHub repository and opens an interactive terminal session to inspect it. Under the hood, this creates a new container (defaults to alpine) and starts a shell, mounting the repository directory inside.

package main

import (
"context"
)

type MyModule struct{}

func (m *MyModule) SimpleDirectory(ctx context.Context) (string, error) {
return dag.
Git("https://github.com/dagger/dagger.git").
Head().
Tree().
Terminal().
File("README.md").
Contents(ctx)
}

The container created to mount the directory can be customized using additional options. The following Dagger Function revised the previous example to demonstrate this, using an ubuntu container image and bash shell instead of the defaults.

package main

import (
"context"
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

func (m *MyModule) AdvancedDirectory(ctx context.Context) (string, error) {
return dag.
Git("https://github.com/dagger/dagger.git").
Head().
Tree().
Terminal(dagger.DirectoryTerminalOpts{
Container: dag.Container().From("ubuntu"),
Cmd: []string{"/bin/bash"},
}).
File("README.md").
Contents(ctx)
}

Example

  • Execute a Dagger Function to clone Dagger's GitHub repository and open a terminal session in the repository directory:

    dagger -c simple-directory
  • Execute another Dagger Function that does the same as the previous one, except using an ubuntu container image as base and initializing the terminal with the bash shell:

    dagger -c advanced-directory

Create custom spans

Dagger represents operations performed by a Dagger Function as OpenTelemetry spans. Spans are typically used to separate tasks that are running in parallel, with each branch waiting for completion.

It is possible to instrument custom OpenTelemetry spans inside any Dagger Function. This allows you to define logical boundaries within complex workflows, measure execution time, and track nested operations with greater granularity. These custom spans appear in the Dagger TUI and Traces.

The following Dagger Function demonstrates this by emitting custom spans for various tasks.

warning

The approach described below is experimental and may be deprecated in favor of a new OpenTelemetry span API. Contribute to the ongoing discussion of this topic on GitHub.

package main

import (
"context"
"main/internal/telemetry"

"golang.org/x/sync/errgroup"
)

type MyModule struct{}

func (m *MyModule) Foo(ctx context.Context) error {
// clone the source code repository
source := dag.
Git("https://github.com/dagger/hello-dagger").
Branch("main").Tree()

// list versions to test against
versions := []string{"20", "22", "23"}

// define errorgroup
eg := new(errgroup.Group)

// run tests concurrently
// emit a span for each
for _, version := range versions {
eg.Go(func() (rerr error) {
ctx, span := Tracer().Start(ctx, "running unit tests with Node "+version)
defer telemetry.End(span, func() error { return rerr })
_, err := dag.Container().
From("node:"+version).
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "test:unit", "run"}).
Sync(ctx)
return err
})
}

return eg.Wait()
}
warning

When using spans to group and measure Dagger API function calls, ensure that the function calls are not lazily evaluated. The duration of the corresponding spans in this case will always be zero.

Example

Execute a Dagger Function to run unit tests on the dagger/hello-dagger source code repository with different versions of Node.js, emitting a custom span for each version tested:

dagger call foo