jdx/mise · error

service unit '{name}' must set a non-empty `exec_start`

Error message

service unit '{name}' must set a non-empty `exec_start`

What it means

A systemd *service* unit must define what to run. `SystemdRequest::from_toml` requires a non-empty `exec_start` (after trimming whitespace) whenever the unit resolves to a Service. Timers are exempt because they reference a companion service instead.

Source

Thrown at src/system/systemd.rs:176

        if !valid_name(&name) {
            bail!("unit name '{name}' must contain only letters, numbers, '.', '_', '-', or '@'");
        }
        let is_timer = config.on_boot_sec.is_some()
            || config.on_unit_active_sec.is_some()
            || config.on_unit_inactive_sec.is_some()
            || config.on_calendar.is_some()
            || config.randomized_delay_sec.is_some()
            || config.accuracy_sec.is_some()
            || config.persistent.is_some()
            || config.unit.is_some();
        let kind = if is_timer {
            SystemdUnitKind::Timer
        } else {
            SystemdUnitKind::Service
        };
        let exec_start = config.exec_start.map(|s| s.trim().to_string());
        if kind == SystemdUnitKind::Service && exec_start.as_deref().is_none_or(str::is_empty) {
            bail!("service unit '{name}' must set a non-empty `exec_start`");
        }
        if kind == SystemdUnitKind::Timer {
            let service_only_fields = [
                (exec_start.is_some(), "exec_start"),
                (config.service_type.is_some(), "type"),
                (config.remain_after_exit.is_some(), "remain_after_exit"),
                (config.exec_stop.is_some(), "exec_stop"),
                (config.timeout_start_sec.is_some(), "timeout_start_sec"),
                (config.timeout_stop_sec.is_some(), "timeout_stop_sec"),
                (config.no_new_privileges.is_some(), "no_new_privileges"),
                (config.private_tmp.is_some(), "private_tmp"),
                (!config.environment.is_empty(), "environment"),
                (!config.environment_file.is_empty(), "environment_file"),
                (config.nice.is_some(), "nice"),
                (config.umask.is_some(), "umask"),
                (config.working_directory.is_some(), "working_directory"),
                (config.restart.is_some(), "restart"),
                (config.restart_sec.is_some(), "restart_sec"),

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Add a non-empty `exec_start` value, e.g. `exec_start = "/usr/bin/myapp --serve"`.
  2. If the unit is meant to be timer-driven, add a timer trigger field (`on_boot_sec`, `on_calendar`, etc.) so it is treated as a Timer and define the schedule there.
  3. Remove the unit entirely if it was added by mistake (`state = "absent"` semantics apply at the service level, not here).

Example fix

# before
[systemd.myapp]
# exec_start missing

# after
[systemd.myapp]
exec_start = "/usr/local/bin/myapp --daemon"
Defensive patterns

Strategy: validation

Validate before calling

fn has_exec_start(cfg: &toml::Value) -> bool {
    cfg.get("exec_start")
        .and_then(|v| v.as_str())
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Declaring `[systemd.<name>]` with no `exec_start` key, or `exec_start = ""` or whitespace-only, on a unit that has no timer trigger fields (so kind == Service).

Common situations: Copy-pasting a timer config and forgetting `exec_start`; commenting out the exec line while debugging; defining a unit intended only to be triggered by an existing timer but configuring it as a plain service.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/63d2d39efe0d045d. Report an issue: GitHub.