skip to content

In Laravel, what do Storage::put(), get(), exists() and delete() return on success and failure, and what does a disk's throw option change?

level: juniorimportance: must knowfreq 56%

answer

  1. booleans for writes, string for reads
  2. get() on failure: null, not false
  3. throw => true raises Flysystem exceptions
  4. report => true logs, still returns
  5. size() is never wrapped

basics

~20 s

put(), 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
<?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

for a junior

Recall that writes return true or false, get() returns the contents or null, and that the disk's throw option switches to exceptions.

for a middle

Explain how the adapter catches Flysystem's UnableTo exceptions, what report adds, and which methods such as size() are not wrapped.

for a senior

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.

for a principal

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