Back to blog
ROS2Tooling

10 Lines in ROS1. 50 in ROS2. The Launch File Problem Nobody Warned You About.

A developer on Reddit described migrating a real project from ROS1 to ROS2. The launch file went from 10 lines to 50. Same nodes. Same topics. Same robot. That is a real measurement from a real migrat

Blai Morte

Blai Morte

Co-Founder

·5 min read

A developer on Reddit described migrating a real project from ROS1 to ROS2. The launch file went from 10 lines to 50. Same nodes. Same topics. Same robot. That is a real measurement from a real migration. It is also a design decision in ROS2 that every production team pays for, usually in debugging time they did not budget.

Why ROS2 Launch Files Are Code, Not Config

ROS1 used XML. ROS2 uses Python. The switch was deliberate — Python launch files can import modules, handle conditionals, loop over parameters, and compose descriptions dynamically. That flexibility is real. It is also why a three-node launch file needs imports, substitutions, and a LaunchDescription object where ROS1 needed three XML tags.

Here is a minimal ROS1 launch file that starts a camera driver, a detector node, and a visualizer:

xml
<launch>
  <node pkg="camera_driver" type="driver" name="camera" />
  <node pkg="detector" type="detect" name="detector" />
  <node pkg="rviz" type="rviz" name="viz" args="-d config.rviz" />
</launch>

Here is the equivalent ROS2 Python launch file:

python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(package='camera_driver', executable='driver', name='camera'),
        Node(package='detector', executable='detect', name='detector'),
        Node(package='rviz2', executable='rviz2', name='viz',
             arguments=['-d', 'config.rviz']),
    ])

That is the simple case. It gets worse.

Where the Verbosity Actually Comes From

Real ROS2 systems need more than node declarations. They need argument handling, namespace remapping, conditional logic, and composable node containers. Each of these adds lines that ROS1 handled implicitly or not at all.

Arguments and substitutions. A launch file that accepts a robot_name parameter and passes it to multiple nodes requires DeclareLaunchArgument, LaunchConfiguration, and TextSubstitution objects. In ROS1, you passed $(arg robot_name) directly in any attribute. In ROS2, every substitution is an object with its own constructor.

Composable node containers. If you want intra-process communication for performance, you do not launch nodes directly. You declare a ComposableNodeContainer, then add ComposableNode actions to it, then handle the edge case where the container crashes but the launch file does not report it. This pattern alone can add 30 lines for a single node group.

Include chains. A production robot often has subsystem launch files: base, perception, navigation, manipulation. Each is included with IncludeLaunchDescription, which requires a PythonLaunchDescriptionSource or XMLLaunchDescriptionSource, a PathJoinSubstitution for the file path, and explicit argument forwarding. In ROS1, <include file="..."/> handled this in one line.

Conditionals. IfCondition and UnlessCondition wrap actions. They require importing the condition class, constructing it with a substitution, and wrapping the target action. One-line if="$(arg use_sim)" in ROS1 becomes five lines in ROS2.

On a real robot, the launch file often becomes the most complex file in the repository. One developer measured it at 3-4x as much time debugging launch files versus debugging C++. The flexibility is real. The verbosity comes with it.

The Debugging Problem

The bigger issue is not the line count. It is that launch files are now executable code that fails silently or fails confusingly.

A syntax error in a Python launch file does not always stop the launch. Depending on where the error occurs, ros2 launch may start some nodes, skip others, and exit with an error message that points to the wrong file in an include chain. Because the launch system evaluates descriptions lazily, a node may be declared but never executed, with no warning that it was omitted.

Composable node containers add another layer. If a node fails to load into the container, the container stays alive and the launch file reports success. You discover the missing node ten minutes later when a downstream topic has no data and ros2 node list does not show it. There is no single command that tells you which nodes in a launch file actually started versus which were declared.

What Teams Actually Do

Most production teams do not write raw launch files for long. They write helper functions that reduce repetition: a launch_robot_node() function that handles the common package, namespace, and remapping boilerplate. Some use YAML-based launch generators that produce Python from a shorter config. Others adopt launch_ros event handlers to catch container load failures.

These adaptations are sensible. They also mean the standard API is missing something most teams need. When everyone writes the same helpers, the default was not built for the common case.

A Practical Reduction

If you are writing a new launch file and want to keep it readable, follow three rules:

1. Use a helper module. Put repeated patterns — node declaration with common remappings, container setup — in a shared Python module imported by all launch files. Do not copy the same 10 lines into every subsystem.

2. Separate static and dynamic parts. Keep node declarations in one file. Keep argument parsing, conditionals, and environment-dependent logic in another. This makes it easier to reason about what is actually launching versus what is deciding whether to launch.

3. Validate before running. Add a --dry-run equivalent by importing your generate_launch_description() in a test and checking that all declared nodes have valid package and executable names. ros2 launch does not do this for you.

What to Take Away

ROS2 switched from XML to Python for a reason: more flexibility. For small projects, that flexibility is overkill. For large projects, the verbosity becomes a maintenance burden that teams patch with their own abstractions. Every new engineer pays the cost in their first afternoon reading launch files instead of robot code.

The launch file is the entry point to your entire system. When it takes 50 lines to express 10 lines of intent, the system is harder to run, harder to debug, and harder to trust.

Continue reading

Related Posts