jdx/mise · error

service unit ' ' must set a non-empty `exec_start

Error message

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

What it means

from_toml requires that every service-kind systemd unit defines exec_start with non-empty (after trimming whitespace) content, since a systemd service without ExecStart is invalid. Timer units are exempt — they only need a scheduling directive.

Solutions

  1. Add a non-empty exec_start to the unit's TOML config (e.g. exec_start = "/usr/bin/myapp --serve")
  2. If the entry is meant to be a timer, add a schedule key (on_boot_sec, on_calendar, etc.) so it is classified as a Timer
  3. Remove the unit entry entirely if it is unused
  4. Trim stray whitespace-only values from exec_start

Example fix

# before (mise.toml)
[systemd."myapp.service"]
# exec_start commented out
# after
[systemd."myapp.service"]
exec_start = "/usr/local/bin/myapp --serve"
Defensive patterns

Strategy: validation

Validate before calling

// pre-validate a service entry before writing config
if schedule_keys.is_empty() && exec_start.as_deref().map(str::trim).unwrap_or("").is_empty() {
    bail!("service needs a non-empty exec_start (or schedule keys to be a timer)");
}

Prevention

When it happens

Trigger: Defining a service unit in mise.toml without an exec_start key, or with exec_start = "" or whitespace-only value, then loading the systemd config through SystemdRequest::from_toml.

Common situations: Writing a timer entry but leaving service fields out of a service block; commenting out exec_start during debugging; copy-pasting a timer template for a service; intending a timer but not setting any schedule field so it defaults to Service kind.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: 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 533346cc37)