How should a Laravel console command report progress, results and failure using `table()`, `withProgressBar()`, `fail()` and its return value?
answer
- line, info, warn, error only print
- table(headers, rows) sizes columns
- withProgressBar: countable iterable or int
- return self::FAILURE or call fail()
- exit code is what cron and CI see
basics
~20 sA Laravel command prints with info(), warn() and error(), shows rows with table() and long loops with withProgressBar(), and signals failure through its exit code: return self::FAILURE or call fail(). Printing an error alone still exits 0.
solid answer
~40 sOutput helpers only print: `$this->info()`, `$this->warn()`, `$this->error()`, `$this->line()` and `$this->newLine()`. `$this->table(['Invoice', 'Customer', 'Error'], $rows)` renders an aligned table from a headers array and an array of rows. `$this->withProgressBar($invoices, fn ($invoice) => $mailer->resend($invoice))` counts the iterable, advances once per item and returns the iterable; pass an integer instead and you advance the bar yourself. Failure is the **exit code**: returning nothing or `self::SUCCESS` exits 0, `return self::FAILURE` exits 1, and `$this->fail('SMTP unavailable')` stops the command at once, prints the message and exits 1. `$this->error('...')` on its own does not change the exit code, so a command that only prints an error still reports success to cron, CI or the scheduler — the classic bug.
go deeper
Recall the output helpers, table() with headers and rows, and that returning self::FAILURE or calling fail() marks failure.
Explain the split between printed output and exit codes, how withProgressBar() handles iterables versus counts, and what fail() does internally.
Make commands automation-friendly: non-zero codes on real failure, summaries and dry-run tables for operators, and deliberate rules for partial success.
Define team conventions for operational commands' output and exit codes so schedulers, CI and alerting can trust them.
## Two separate channels A console command talks to two audiences. **Humans** read its output: messages, tables, progress bars. **Machines** — cron, CI pipelines, deploy scripts, the scheduler, a parent command — read its **exit code**: `0` for success, anything else for failure. Laravel gives you helpers for both, and they are independent. The most common bug in custom commands is treating the first as if it were the second. ## Messages - `$this->line('text')` — plain text. - `$this->info('Re-sent 12 emails.')` — success-styled text. - `$this->comment()`, `$this->question()`, `$this->warn()` — other styles. - `$this->error('Customer 42 has no failed invoices.')` — error-styled text, still just output. - `$this->newLine(2)` — blank lines. - `$this->components->info()`, `->warn()`, `->error()`, `->task('Sending', fn () => ...)` and `->twoColumnDetail()` — the framed, badge-style output Laravel's own commands use. Each message helper also takes a verbosity argument, so `$this->info('Batch 3 done', 'v')` appears only with `-v`, and `--quiet` suppresses ordinary output entirely. ## Tables `$this->table($headers, $rows)` takes an array of column headings and an array of rows — each row an array of cell values in the same order — and computes column widths for you: ```php $this->table( ['Invoice', 'Customer', 'Failed at', 'Error'], $failed->map(fn ($i) => [$i->number, $i->customer_id, $i->failed_at, $i->last_error])->all(), ); ``` A dry-run mode that prints a table of what *would* be re-sent is one of the best uses: the operator sees exactly what the real run will touch. ## Progress bars `withProgressBar()` accepts either an iterable or a total count: 1. **Iterable** — `$this->withProgressBar($invoices, function ($invoice, $bar) { ... })` calls `count()` on the iterable, starts the bar, calls your closure for each item, advances after each one, finishes the bar and returns the iterable. The value must be countable: an array or collection works, a plain generator does not. 2. **Integer** — `$this->withProgressBar(500, function ($bar) { ... })` calls your closure once with the bar, and you call `$bar->advance()` yourself. 3. For full control, `$this->output->createProgressBar($total)` returns the underlying progress bar with `start()`, `advance()` and `finish()`. Laravel Prompts offers a `progress()` function with a label and hint if you prefer its styling. ## Exit codes | What `handle()` does | Exit code | |---|---| | returns nothing, or returns `self::SUCCESS` | 0 | | returns `self::FAILURE` | 1 | | returns `self::INVALID` | 2 | | returns another integer | that integer | | calls `$this->fail('message')` | 1, after printing the message as an error | | lets an exception escape | non-zero, with the exception rendered | `fail()` throws internally, so it works from any helper method deep inside the command, not only from `handle()`. Passing it an exception object rethrows that exception instead. ## Verbosity and quiet runs Output helpers respect the console's verbosity. Ordinary messages appear at normal verbosity; a helper given a verbosity level such as `'v'` or `'vv'` prints only when the operator asks for more detail with `-v` or `-vv`; `--quiet` suppresses ordinary output. That makes it cheap to leave diagnostic lines in an operational command — which invoice is being processed, how long each batch took — without cluttering the default run. Because the exit code is independent of verbosity, a quiet run still tells automation whether it succeeded, which is exactly what a scheduled run needs. ## The classic bug ```php if ($failures > 0) { $this->error("{$failures} emails could not be sent."); } // falls through: exit code 0 ``` The operator sees red text; cron, CI and the scheduler see success, so no alert fires and a retry never happens. Fix it by returning `self::FAILURE` or by calling `$this->fail(...)`. ## A reporting pattern for operational commands - Print a short summary line at the end (`Re-sent 12 of 14 emails`), even on success. - Show details in a table only when they help — dry runs, failures. - Use a progress bar for loops that take more than a few seconds. - Decide deliberately what counts as failure: one email out of a hundred failing may be partial success, but it should still produce a non-zero code if someone needs to act.
- Why does `withProgressBar()` fail when you pass it a generator, and what can you do instead?With an iterable it calls `count()` first to size the bar, and a plain generator is not countable. Pass an array, a collection or a countable query result, or pass the total as an integer — for example from a `count()` query — and call `$bar->advance()` inside the loop yourself.
- What is the difference between `$this->fail('SMTP down')` and `return self::FAILURE` in a Laravel command?Both end with exit code 1. `fail()` throws internally, so it stops execution immediately from any method, prints the message as an error and never runs the rest of `handle()`; returning `self::FAILURE` is an ordinary return from `handle()`, so you print your own message first. Use `fail()` from helpers deep in the command, and a return where the flow is simple.
saying these in an interview costs you the question
- Calling $this->error() makes the command exit with a non-zero code.
- A handle() method that returns nothing exits with an undefined status.
- withProgressBar() works with any iterable, including plain generators.
- fail() can only be called directly inside handle().
- Exit codes don't matter because humans read the output anyway.