In Laravel, what do Storage::put(), get(), exists() and delete() return on success and failure, and what does a disk's throw option change?
answer
- booleans for writes, string for reads
- get() on failure: null, not false
- throw => true raises Flysystem exceptions
- report => true logs, still returns
- size() is never wrapped
basics
~20 sput(), delete(), copy() and move() return true or false; get() returns the contents or null; exists() returns a boolean. With a disk's throw option true, failures raise League\Flysystem exceptions such as UnableToWriteFile instead of returning false or null.
solid answer
~40 s`Storage::put()` returns `true` on success and `false` when the write fails; `delete()`, `copy()`, `move()` and `setVisibility()` follow the same pattern. `get()` returns the file contents as a string, or `null` when the read fails, and `json()` decodes that or returns `null`. `exists()` returns a boolean and is true for a file or a directory. Laravel's adapter catches Flysystem's `UnableTo...` exceptions and turns them into those return values unless the disk config sets `'throw' => true`, in which case the exception, for example `League\Flysystem\UnableToWriteFile`, reaches your code. `'report' => true` sends the exception to the exception handler but still returns `false` or `null`. A few methods are not wrapped at all: `size()` and `lastModified()` throw `UnableToRetrieveMetadata` for a missing file whatever `throw` says.
code
php · 18 lines<?php
use Illuminate\Support\Facades\Storage;
use League\Flysystem\FilesystemException;
// Default disk config: 'throw' => false
if (! Storage::disk('documents')->put($path, $pdf)) {
throw new RuntimeException("Could not store {$path}");
}
$pdf = Storage::disk('documents')->get($path) ?? abort(404);
// On a disk configured with 'throw' => true
try {
Storage::disk('archive')->put($path, $pdf);
} catch (FilesystemException $e) {
report($e);
}go deeper
Recall that writes return true or false, get() returns the contents or null, and that the disk's throw option switches to exceptions.
Explain how the adapter catches Flysystem's UnableTo exceptions, what report adds, and which methods such as size() are not wrapped.
Show how silent false returns become data-loss bugs and choose throw, report or explicit checks per disk based on how critical the files are.
Set a team rule for storage failure handling so critical disks fail loudly and best-effort writes are marked as such in code.
## The Storage API in one paragraph Laravel's `Storage` facade talks to a **disk**, a configured storage location backed by a Flysystem adapter (local folder, S3 bucket, SFTP server). The facade methods are defined on `Illuminate\Filesystem\FilesystemAdapter`, which wraps Flysystem and decides what your code sees when an operation fails. That decision is the subject of this question: Flysystem itself throws an exception for every failure, and Laravel converts most of them into return values. ## Return values at a glance | Method | Success | Failure with `throw` false | Failure with `throw` true | |---|---|---|---| | `put($path, $contents)` | `true` | `false` | `UnableToWriteFile` (or `UnableToSetVisibility`) | | `get($path)` | file contents (string) | `null` | `UnableToReadFile` | | `json($path)` | decoded array | `null` | `UnableToReadFile` | | `exists($path)` | `true` / `false` | not wrapped | not wrapped | | `delete($paths)` | `true` | `false` | `UnableToDeleteFile` | | `copy($from, $to)` | `true` | `false` | `UnableToCopyFile` | | `move($from, $to)` | `true` | `false` | `UnableToMoveFile` | | `mimeType($path)` | MIME string | `false` | `UnableToRetrieveMetadata` | | `size($path)` | bytes (int) | throws `UnableToRetrieveMetadata` | throws | A few details hide in that table: - **`get()` returns `null`, not `false`.** The method catches the exception and simply returns nothing, so `if (Storage::get($p) === false)` never matches. - **`delete()` of a file that is not there is not a failure** on the local driver: the adapter returns early and Laravel reports `true`. - **`exists()` means file *or* directory.** Use `fileExists()` or `directoryExists()` when the difference matters; `missing()` is simply `! exists()`. - **Metadata calls are uneven.** `mimeType()` and `checksum()` are wrapped; `size()` and `lastModified()` pass straight to Flysystem and throw for a missing file. ## The throw and report options Both options live in the disk's array in `config/filesystems.php`, and the Laravel 13 skeleton sets both to `false` on every disk: 1. **`'throw' => false`** (default): failures become `false` or `null`. Nothing is logged unless `report` is on. 2. **`'report' => true`**: the caught exception is passed to the application's exception handler (so it reaches your logs), and the method still returns `false` or `null`. 3. **`'throw' => true`**: the exception is rethrown, and your code, or the framework's error handling, deals with it. The exception classes all live in `League\Flysystem` and share the `FilesystemException` interface, so one `catch (FilesystemException $e)` covers every storage failure. ## Writing code that notices failures Picture a clinic portal that stores patients' lab-report PDFs on a `documents` disk. With the defaults, this is a silent data-loss bug: ```php Storage::disk('documents')->put($path, $pdf); $report->update(['path' => $path]); ``` If the write fails, the database now points at a file that does not exist. Three honest fixes: - check the boolean: `if (! Storage::disk('documents')->put($path, $pdf)) { ... }`; - set `'throw' => true` on disks the app cannot work without, so a failure aborts the request or job; - at minimum set `'report' => true`, so failures appear in the logs. On the read side, `get()` returning `null` pairs well with an explicit 404: `$pdf = Storage::disk('documents')->get($path) ?? abort(404);`. ## Existence checks and their pitfalls Checking before acting is common, but each check is its own operation: - `exists()` is a single call on the local disk, while on remote disks a miss can cost two requests, because Flysystem checks for a file and then for a directory. - A check followed by a read is not atomic: the file can disappear in between. On critical paths, prefer calling `get()` and handling `null`, or enable `throw` and catch the exception, rather than trusting an earlier `exists()`. - `missing()`, `fileMissing()` and `directoryMissing()` are just negations, useful for readable guards such as `abort_if(Storage::disk('documents')->fileMissing($path), 404)`. The same thinking applies to deletes: since removing an absent file reports success on the local driver, `delete()` returning `true` tells you the file is gone, not that it was there. ## Why the defaults are lenient Returning `false` keeps simple scripts simple and mirrors PHP's own file functions, which return `false` rather than throwing. The cost is that errors are easy to ignore. Many teams turn `throw` on for their main disks and keep the boolean style only where a failure is genuinely acceptable, such as best-effort cache files.
- Why can Storage::size() throw even when the disk's throw option is false?`size()` and `lastModified()` call Flysystem directly without Laravel's try/catch, so Flysystem's `UnableToRetrieveMetadata` for a missing file always propagates. `mimeType()` and `checksum()` are wrapped and return `false` instead. Check `fileExists()` first or catch the exception when the file may be absent.
- What is the difference between the throw and report options on a disk?`throw` decides whether the Flysystem exception reaches your code. `report` decides whether a caught exception is sent to the exception handler for logging. With `report` on and `throw` off, the method still returns `false` or `null`, but the failure is visible in the logs.
saying these in an interview costs you the question
- Storage::get() returns false when the file cannot be read
- put() throws an exception by default when the disk is full or unreachable
- exists() is true only for files, never for directories
- report => true makes a failed put() throw after logging
- size() returns 0 for a missing file