Downloading a file from a URL in C# looks like a one-liner, and it is. The hard part is what happens next: the file doesn't exist, the destination folder is missing, you accidentally overwrite something important. Skip the easy parts and you spend your afternoon writing retry logic.
According to code4it.dev, there are a few clean patterns for pulling a file off the web, handling failures, and saving it to disk without trashing your filesystem.
At a glance:
- `HttpClient.GetStreamAsync(url)` returns a stream you can copy to disk.
- Errors throw `HttpRequestException`, which you can catch and convert to `Stream.Null`.
- `Directory.CreateDirectory` creates missing folders before writing.
- `FileMode.Create` overwrites existing files; `FileMode.CreateNew` throws if the file exists.
- `HttpClientFactory` is preferred over a raw `HttpClient` for repeated calls.
The one-liner that actually works
Strip away the edge cases and the basic download fits in three lines. You build a client, ask for the stream, and pipe the bytes somewhere useful:
“`csharp
Stream stream = await client.GetStreamAsync(fileUrl);
Await stream.CopyToAsync(destination);
“`
That's the happy path. According to code4it.dev, this is fine for a quick script, but in a long-running application you should inject `HttpClient` from `HttpClientFactory` instead. Raw client instances can lead to socket exhaustion under load because they don't reuse connections efficiently.
`HttpClientFactory` manages the underlying handler pool for you and avoids the classic "ran out of sockets" bug that hits apps with many concurrent requests.
Handling the errors you'll actually hit
Two failures stop most downloads: the file doesn't exist (HTTP 404), or the file requires authentication (HTTP 401 or 403). Both throw `HttpRequestException` because `GetStreamAsync` calls `EnsureSuccessStatusCode` under the hood.
According to code4it.dev, the cleanest pattern is to return `Stream.Null` from your download method so the caller can check explicitly:
“`csharp
Async Task<Stream> FetchStream(string url)
{
Var client = httpClientFactory.CreateClient();
Try
{
Return await client.GetStreamAsync(url);
}
Catch (Exception ex)
{
Return Stream.Null;
}
}
“`
Note the trade-off: returning `Stream.Null` hides the difference between "file missing" and "auth required." For production code, throw a custom exception with context instead. The reference article uses `Stream.Null` for clarity, but real apps should log the failure mode.
Writing the stream to disk
Once you have a stream, drop it into an output file stream wrapped in a `using` block so the handle closes cleanly. Pick the right `FileMode` to decide what happens when the target file already exists:
“`csharp
Using (FileStream destination = new FileStream(path, FileMode.CreateNew))
{
Await sourceStream.CopyToAsync(destination);
}
“`
The `FileMode` flag is the lever you control:
- `FileMode.Create`: overwrite the file. Useful for cache rebuilds, dangerous for user data.
- `FileMode.CreateNew`: throws `IOException` if the file exists. Safer for one-shot downloads.
- `FileMode.Append`: add to the existing file. Rarely what you want.
Pick deliberately. The default of `FileMode.Create` will silently overwrite a file the user didn't expect to lose.
Edge cases that bite
Two edge cases come up constantly, both covered by code4it.dev.
First, the destination folder may not exist. If you try to write to `C:dataarchivefile.png` and `C:dataarchive` doesn't exist, you'll get `DirectoryNotFoundException`. Fix it by calling `Directory.CreateDirectory(path)` before opening the file. That call creates the full path hierarchy, not just the last segment.
Second, the destination file may already exist. `FileMode.Create` overwrites. `FileMode.CreateNew` throws. Choose based on whether the user expects the file to be replaced or not.
Other edge cases worth coding for: zero-byte responses (the stream is valid but contains nothing), network timeouts (catch `TaskCanceledException`), and partial writes if the connection drops mid-download.
Worked example: a safe download helper
Say you're building an app that pulls product images from a CDN. You want a helper method that downloads an image, creates the folder if missing, and refuses to overwrite existing files:
“`csharp
Async Task DownloadAndSave(string sourceUrl, string folder, string fileName)
{
Var stream = await FetchStream(sourceUrl);
If (stream != Stream.Null)
{
If (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
String fullPath = Path.Combine(folder, fileName);
Using (FileStream destination = new FileStream(fullPath, FileMode.CreateNew))
{
Await stream.CopyToAsync(destination);
}
}
}
“`
`FileMode.CreateNew` means an existing file at that path triggers an `IOException`, which you can catch and log. Better a clear error than a silent overwrite.
Our analysis
File downloads look simple because the happy path is short. Production code lives in the unhappy paths. The two most expensive bugs in real apps are socket exhaustion (use `HttpClientFactory`) and silent file overwrites (pick the right `FileMode`).
Most teams skip the failure paths because they don't show up in unit tests. They show up in production when a CDN returns 403, when a user reinstalls the app and the cache folder is gone, or when a flaky mobile network drops mid-download. The cost of fixing those is far higher than the cost of writing three lines of `try`/`catch` upfront.
What to watch next: `HttpClient` in .NET 8+ is moving toward more sane defaults around connection lifetime, but the patterns above still apply. Don't roll your own retry logic either; let `Polly` or the built-in resilience handlers handle backoff.
Bottom line: download through `HttpClientFactory`, validate the stream, choose the right `FileMode`, and create folders before writing. The whole thing fits in twenty lines if you're not sloppy.
Key takeaways
- Use `HttpClientFactory` for repeated downloads, not raw `HttpClient`.
- Catch `HttpRequestException` and decide between `Stream.Null` or a custom exception.
- Pick `FileMode.Create` or `CreateNew` deliberately based on overwrite intent.
- Call `Directory.CreateDirectory` before writing to a folder that may not exist.
- Use `using` blocks on file streams to avoid leaking file handles.
Frequently asked questions
Why use HttpClientFactory instead of HttpClient directly?
`HttpClientFactory` manages connection pooling and handler lifetimes. Raw `HttpClient` instances can leak sockets under heavy load.
What exception does GetStreamAsync throw on a 404?
`HttpRequestException`, because `GetStreamAsync` calls `EnsureSuccessStatusCode` internally.
How do I avoid overwriting an existing file?
Use `FileMode.CreateNew` instead of `FileMode.Create`. It throws `IOException` if the file already exists.
How do I create a missing destination folder?
Call `Directory.CreateDirectory(path)` before opening the file. It creates the full hierarchy.
How do I tell if a download actually succeeded?
Return `Stream.Null` from your download method on failure, then check the result before writing to disk.