This Firebase Storage error occurs when a file upload or download operation is canceled before completion. The cancellation can be intentional (user clicks cancel button) or unintentional (network loss, app backgrounding, or promise rejection). Understanding when to allow cancellation versus retrying is key to handling this error gracefully.
The "storage/canceled" error in Firebase Storage is raised when a file transfer operation (upload or download) is deliberately stopped before completion. This is a normal error that developers should expect to handle in their applications, unlike other storage errors which typically indicate problems. Firebase Storage provides the `cancel()` method on upload and download tasks specifically to allow users to stop transfers. When this method is called, any associated promises are rejected with the "storage/canceled" error code. This error can also occur if the underlying task is interrupted by uncontrollable factors like network loss, app suspension, or unhandled promise rejections. The key distinction is that "canceled" is not an error in the system-failure senseโit's an intentional operational state. Your code must distinguish between user-initiated cancellations (which may be normal) and unexpected interruptions (which might need logging or cleanup).
Always check for the "storage/canceled" error code when implementing upload or download functionality:
// Web SDK example
import { ref, uploadBytesResumable } from 'firebase/storage';
function uploadFile(storage, filePath, file) {
const storageRef = ref(storage, filePath);
const uploadTask = uploadBytesResumable(storageRef, file);
return new Promise((resolve, reject) => {
uploadTask.on('state_changed',
// Progress
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload progress:', progress + '%');
},
// Error handler - CRITICAL
(error) => {
if (error.code === 'storage/canceled') {
// User canceled - this is expected behavior
console.log('Upload was canceled by user');
reject({ message: 'Upload canceled', canceled: true });
} else {
// Unexpected error
console.error('Upload error:', error);
reject(error);
}
},
// Complete
() => {
console.log('Upload completed successfully');
resolve(uploadTask.snapshot);
}
);
});
}
// Usage in React
import { useState } from 'react';
export function FileUploader({ storage }) {
const [uploading, setUploading] = useState(false);
const [uploadTask, setUploadTask] = useState(null);
const handleFileSelect = async (e) => {
const file = e.target.files[0];
if (!file) return;
setUploading(true);
try {
await uploadFile(storage, `uploads/${file.name}`, file);
console.log('File uploaded successfully');
} catch (error) {
if (error.canceled) {
// User canceled - update UI appropriately
console.log('Upload was canceled');
} else {
// Unexpected error
console.error('Upload failed:', error.message);
}
} finally {
setUploading(false);
}
};
return (
<input
type="file"
onChange={handleFileSelect}
disabled={uploading}
/>
);
}The key is checking error.code === 'storage/canceled' to differentiate user cancellation from actual errors.
Allow users to cancel uploads/downloads but ensure event listeners are cleaned up:
// Web SDK - Cancelable upload with cleanup
import { ref, uploadBytesResumable } from 'firebase/storage';
import { useState } from 'react';
export function CancelableUploader({ storage }) {
const [uploadTask, setUploadTask] = useState(null);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (!file) return;
setUploading(true);
const storageRef = ref(storage, `uploads/${file.name}`);
const task = uploadBytesResumable(storageRef, file);
setUploadTask(task);
task.on('state_changed',
(snapshot) => {
const percent = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
setProgress(percent);
},
(error) => {
if (error.code === 'storage/canceled') {
console.log('Upload canceled');
} else {
console.error('Upload error:', error);
}
setUploading(false);
},
() => {
console.log('Upload complete');
setUploading(false);
}
);
};
const handleCancel = () => {
if (uploadTask) {
// cancel() returns a promise that resolves to false if already finished
uploadTask.cancel().then(() => {
console.log('Cancel requested');
}).catch((error) => {
console.error('Cancel failed:', error);
});
}
};
return (
<div>
<input
type="file"
onChange={handleFileSelect}
disabled={uploading}
/>
{uploading && (
<>
<progress value={progress} max="100" />
<button onClick={handleCancel}>Cancel Upload</button>
</>
)}
</div>
);
}Important: task.cancel() returns a boolean promiseโit returns true if cancellation was successful, false if the task had already completed.
Log and handle the two scenarios differently:
import { ref, uploadBytesResumable } from 'firebase/storage';
function uploadWithLogging(storage, filePath, file) {
const storageRef = ref(storage, filePath);
const uploadTask = uploadBytesResumable(storageRef, file);
let userInitiatedCancel = false;
// Track if user explicitly canceled
const handleUserCancel = () => {
userInitiatedCancel = true;
uploadTask.cancel();
};
return new Promise((resolve, reject) => {
uploadTask.on('state_changed',
(snapshot) => {
// Progress update
},
(error) => {
if (error.code === 'storage/canceled') {
if (userInitiatedCancel) {
// Expected - user clicked cancel button
console.log('Upload canceled by user');
reject({ message: 'Canceled by user', userInitiated: true });
} else {
// Unexpected - network or app interruption
console.warn('Upload canceled unexpectedly (network or app issue)');
reject({ message: 'Upload interrupted', userInitiated: false });
}
} else {
console.error('Upload error:', error);
reject(error);
}
},
() => {
console.log('Upload completed');
resolve(uploadTask.snapshot);
}
);
// Return cancel function for UI
return { cancel: handleUserCancel };
});
}This pattern helps distinguish user-intentional cancellation from system interruptions.
For temporary interruptions, prefer pause/resume over cancellation:
import { ref, uploadBytesResumable } from 'firebase/storage';
export function PauseableUploader({ storage }) {
const [uploadTask, setUploadTask] = useState(null);
const [isPaused, setIsPaused] = useState(false);
const handleFileSelect = (e) => {
const file = e.target.files[0];
const storageRef = ref(storage, `uploads/${file.name}`);
const task = uploadBytesResumable(storageRef, file);
setUploadTask(task);
task.on('state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Progress:', progress);
},
(error) => {
console.error('Upload error:', error);
},
() => {
console.log('Upload complete');
}
);
};
const handlePause = () => {
if (uploadTask && !isPaused) {
uploadTask.pause();
setIsPaused(true);
console.log('Upload paused');
}
};
const handleResume = () => {
if (uploadTask && isPaused) {
uploadTask.resume();
setIsPaused(false);
console.log('Upload resumed');
}
};
const handleCancel = () => {
if (uploadTask) {
uploadTask.cancel().then(() => {
console.log('Upload canceled');
setUploadTask(null);
});
}
};
return (
<div>
<button onClick={handlePause} disabled={!uploadTask || isPaused}>
Pause
</button>
<button onClick={handleResume} disabled={!isPaused}>
Resume
</button>
<button onClick={handleCancel}>Cancel</button>
</div>
);
}Key difference: pause() can be resumed, cancel() restarts the entire upload.
Prevent orphaned uploads and unhandled promise rejections by cleaning up on unmount:
import { useState, useEffect, useRef } from 'react';
import { ref, uploadBytesResumable } from 'firebase/storage';
export function ManagedFileUploader({ storage }) {
const [uploading, setUploading] = useState(false);
const uploadTaskRef = useRef(null);
useEffect(() => {
// Cleanup function - runs when component unmounts
return () => {
if (uploadTaskRef.current && uploading) {
console.log('Component unmounting, canceling upload');
uploadTaskRef.current.cancel().catch(() => {
// Ignore errors from canceling
});
}
};
}, [uploading]);
const handleFileSelect = async (e) => {
const file = e.target.files[0];
if (!file) return;
setUploading(true);
const storageRef = ref(storage, `uploads/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTaskRef.current = uploadTask;
return new Promise((resolve, reject) => {
uploadTask.on('state_changed',
(snapshot) => {
if (!uploading) {
uploadTask.cancel();
return;
}
},
(error) => {
if (error.code === 'storage/canceled') {
console.log('Upload was canceled');
} else {
console.error('Upload failed:', error);
}
setUploading(false);
reject(error);
},
() => {
console.log('Upload complete');
setUploading(false);
resolve(uploadTask.snapshot);
}
);
});
};
return <input type="file" onChange={handleFileSelect} disabled={uploading} />;
}The cleanup function ensures uploads are properly canceled before the component unmounts.
Auto-retry failed uploads, but don't retry if user explicitly canceled:
import { ref, uploadBytesResumable } from 'firebase/storage';
async function uploadWithAutoRetry(storage, filePath, file, maxRetries = 3) {
let userCanceled = false;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
if (userCanceled) {
console.log('Skipping retry - user canceled');
break;
}
const storageRef = ref(storage, filePath);
const uploadTask = uploadBytesResumable(storageRef, file);
try {
await new Promise((resolve, reject) => {
uploadTask.on('state_changed',
(snapshot) => {
// Progress
},
(error) => {
if (error.code === 'storage/canceled') {
userCanceled = true;
}
reject(error);
},
() => resolve(uploadTask.snapshot)
);
// Provide cancel method
window.cancelCurrentUpload = () => {
userCanceled = true;
uploadTask.cancel();
};
});
// Success
return { success: true, attempt };
} catch (error) {
lastError = error;
if (userCanceled) {
throw new Error('Upload canceled by user');
}
if (attempt < maxRetries && error.code !== 'storage/canceled') {
const delay = Math.pow(2, attempt - 1) * 1000;
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}Respecting user cancellation prevents wasting bandwidth on unwanted retries.
### Task Cancellation States and Behaviors
Firebase Storage tasks transition through different states when canceled:
Running State: Task actively transferring data. task.cancel() returns true and triggers "canceled" error immediately.
Paused State: task.pause() was called previously. Calling task.cancel() still returns true and triggers "canceled" error.
Completed State: Task already finished successfully. task.cancel() returns false, no error triggered.
Failed State: Task encountered an error. task.cancel() returns false, original error is preserved.
### Platform-Specific Considerations
Android:
- May throw error: "unable to change internal state to: INTERNAL_STATE_CANCELED isUser: true from state:INTERNAL_STATE_IN_PROGRESS"
- Indicates runtime timing issue when changing task state
- Restarting app typically resolves stuck CANCELING state
- Use isInProgress() instead of isCanceled() for status checks
iOS:
- Cancellation works reliably with putFile() and putData()
- App backgrounding may pause uploads automatically
- Must handle explicit cancel on app restore
Node.js/Admin SDK:
- No direct task-level cancellation API
- Use AbortController for fetch-based uploads
- Server-side uploads are more stable with less spontaneous cancellation
React Native/Expo:
- Different behavior on iOS vs Android
- Always test on target platforms
- Image picker files are temporary, ensure stable before upload
### Promise Rejection Handling
Critical: Always attach error handlers BEFORE calling cancel() to prevent unhandled rejection warnings:
// CORRECT - Attach handlers first
uploadTask.on('state_changed', null, (error) => {
console.error(error);
});
uploadTask.cancel();
// WRONG - Will cause unhandled rejection
uploadTask.then(() => {}).catch(err => {});
uploadTask.cancel(); // Error not caught yet### Pause vs Cancel
| Operation | Can Resume | Error Thrown | Use Case |
|-----------|-----------|------------|----------|
| pause() | Yes | No | Temporary interruption |
| cancel() | No | storage/canceled | Permanent stop |
Canceled tasks must restart from the beginning. Paused tasks can resume from where they stopped.
### Debugging Cancellation Issues
Monitor task lifecycle:
uploadTask.on('state_changed',
(snapshot) => {
console.log('State:', snapshot.state);
console.log('Progress:', snapshot.bytesTransferred, '/', snapshot.totalBytes);
},
(error) => {
console.error('Error code:', error.code);
console.error('Message:', error.message);
}
);### Known Issues
- StorageTask cannot be canceled while network connection unavailable (returns true but listener not called until connection restored)
- Canceled transfers can't be resumed (use pause() if resumption needed)
- On older Android versions, internal state change errors may occur
- Promise rejections from canceled tasks may appear unhandled if listeners not attached
Callable Functions: INTERNAL - Unhandled exception
How to fix "Callable Functions: INTERNAL - Unhandled exception" in Firebase
messaging/UNSPECIFIED_ERROR: No additional information available
How to fix "messaging/UNSPECIFIED_ERROR: No additional information available" in Firebase Cloud Messaging
auth/invalid-hash-algorithm: Hash algorithm doesn't match supported options
How to fix "auth/invalid-hash-algorithm: Hash algorithm doesn't match supported options" in Firebase
App Check: reCAPTCHA Score Too Low
App Check reCAPTCHA Score Too Low
storage/invalid-url: Invalid URL format for Cloud Storage reference
How to fix invalid URL format in Firebase Cloud Storage