skip to content

Reads, Writes & Temporary URLs

The Storage API reads, writes, copies and streams files on any disk and hands out plain or signed temporary URLs. Interviewers probe serving private files and what remote-disk calls cost.

on this pageshow

explore

questions

6

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
open as a page

In Laravel, when do you use Storage::url() versus Storage::temporaryUrl(), for example for private medical documents that should download for ten minutes?

level: middleimportance: must knowfreq 62%

basics

~10 s

Storage::url() builds a permanent, unsigned link that works only for publicly readable files. Storage::temporaryUrl($path, now()->addMinutes(10)) builds a signed link that expires, which suits private medical documents once the user has been authorized.

open as a page

In Laravel, how do Storage::download() and Storage::response() differ, and what does each cost when the file sits on an S3 disk?

level: middleimportance: should knowfreq 42%

basics

~20 s

Both return a StreamedResponse that pipes the file through PHP: response() sends an inline disposition, download() an attachment. On S3 each first looks up the MIME type and size, then relays the object through your server.

open as a page

In Laravel, why can Storage::put($path, file_get_contents($tmp)) exhaust memory on a 2 GB scan archive, and how do putFile, putFileAs and streams avoid it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Passing a string means the whole 2 GB file sits in PHP memory. putFile and putFileAs open the file as a stream and write it chunk by chunk; put() with a resource, readStream()/writeStream() and copyToDisk() stream the same way.

open as a page

In Laravel, how does Storage::temporaryUploadUrl() let a browser upload a patient's scan straight to S3, and what must the app still do after the upload?

level: seniorimportance: should knowfreq 30%

basics

~20 s

temporaryUploadUrl($path, $expiresAt) returns an array with a signed url and the headers the browser must send; on S3 it is a presigned PutObject. The app still chooses the key, authorizes, and afterwards verifies the object and records it.

open as a page

In Laravel, what does Storage::append() actually do on an S3 disk, and why are append() and allFiles() expensive on remote disks?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

Storage::append() downloads the whole file, adds the new data and uploads everything again, so each call costs the full file and concurrent calls can lose lines. allFiles() lists every object recursively into one array.

open as a page